diff --git a/.github/scripts/check_no_private_symbols.py b/.github/scripts/check_no_private_symbols.py new file mode 100755 index 00000000..4412a9af --- /dev/null +++ b/.github/scripts/check_no_private_symbols.py @@ -0,0 +1,247 @@ +#!/usr/bin/env python3 +# Copyright 2026 LiveKit +# +# 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. +# +""" +Verify that liblivekit's exported ABI does not leak private dependency symbols. + +The LiveKit SDK statically links several private dependencies (spdlog, fmt, +google::protobuf, absl). When those symbols escape the dynamic symbol table +of liblivekit.{so,dylib,dll}, they collide at runtime with the same libraries +loaded elsewhere in the host process (a common failure mode is ROS 2's +rcl_logging_spdlog ABI-clashing with our vendored spdlog and crashing inside +spdlog::pattern_formatter). + +This script lists exported defined symbols from the supplied shared library +using the platform-appropriate tool and fails (exit code 1) if any of them +match a forbidden pattern. + +Usage: + python3 check_no_private_symbols.py + +Optional environment variables: + LIVEKIT_SYMBOL_CHECK_VERBOSE=1 Print every leaked symbol (default: print + up to 20 examples). + LIVEKIT_SYMBOL_CHECK_EXTRA_FORBIDDEN=foo,bar Additional comma-separated + patterns to forbid. +""" + +import os +import re +import shutil +import subprocess +import sys +from pathlib import Path + +# Substring patterns that must NOT appear in any exported symbol after +# demangling. We use plain-substring semantics for readability; if you need a +# regex, switch to re.search. +DEFAULT_FORBIDDEN = [ + "spdlog::", + "fmt::v", + "google::protobuf", + "absl::", +] + +MAX_REPORTED_LEAKS = 20 + + +def _which_or_die(name: str) -> str: + path = shutil.which(name) + if not path: + sys.stderr.write(f"error: required tool '{name}' not found on PATH\n") + sys.exit(2) + return path + + +def _list_exports_macos(lib: Path) -> list[str]: + nm = _which_or_die("nm") + cxxfilt = shutil.which("c++filt") + # -gU: external (global) defined symbols. + raw = subprocess.run( + [nm, "-gU", str(lib)], + check=True, + capture_output=True, + text=True, + ).stdout + if cxxfilt: + raw = subprocess.run( + [cxxfilt], + input=raw, + check=True, + capture_output=True, + text=True, + ).stdout + return raw.splitlines() + + +def _list_exports_linux(lib: Path) -> list[str]: + nm = _which_or_die("nm") + cxxfilt = shutil.which("c++filt") + # -D: dynamic symbols (i.e., what's actually visible to the dynamic linker) + # --defined-only: drop UND entries + raw = subprocess.run( + [nm, "-D", "--defined-only", str(lib)], + check=True, + capture_output=True, + text=True, + ).stdout + if cxxfilt: + raw = subprocess.run( + [cxxfilt], + input=raw, + check=True, + capture_output=True, + text=True, + ).stdout + return raw.splitlines() + + +def _find_dumpbin_via_vswhere() -> str | None: + """Locate dumpbin.exe under the latest installed Visual Studio. + + GitHub-hosted Windows runners (and standard local VS installs) don't add + dumpbin to PATH unless the user opens a Developer Command Prompt. vswhere + ships at a fixed location with every Visual Studio install since 2017 and + is the supported way to discover the install tree from a vanilla shell. + """ + program_files_x86 = os.environ.get( + "ProgramFiles(x86)", r"C:\Program Files (x86)" + ) + vswhere = Path(program_files_x86) / "Microsoft Visual Studio" / "Installer" \ + / "vswhere.exe" + if not vswhere.exists(): + return None + try: + proc = subprocess.run( + [ + str(vswhere), + "-latest", + "-products", "*", + "-requires", "Microsoft.VisualStudio.Component.VC.Tools.x86.x64", + "-property", "installationPath", + ], + check=True, + capture_output=True, + text=True, + ) + except subprocess.CalledProcessError: + return None + install_path = proc.stdout.strip() + if not install_path: + return None + msvc_root = Path(install_path) / "VC" / "Tools" / "MSVC" + if not msvc_root.is_dir(): + return None + # Pick the highest-versioned toolchain present (lexicographic order matches + # version order for dotted MSVC versions like "14.44.35207"). + for version_dir in sorted(msvc_root.iterdir(), reverse=True): + candidate = version_dir / "bin" / "Hostx64" / "x64" / "dumpbin.exe" + if candidate.exists(): + return str(candidate) + return None + + +def _list_exports_windows(lib: Path) -> list[str]: + # dumpbin ships with MSVC; it understands import libs (.lib) and DLLs. + dumpbin = shutil.which("dumpbin") or _find_dumpbin_via_vswhere() + if not dumpbin: + sys.stderr.write( + "error: 'dumpbin' not on PATH and could not be located via " + "vswhere; run from a Visual Studio Developer command prompt or " + "ensure dumpbin.exe is available\n" + ) + sys.exit(2) + raw = subprocess.run( + [dumpbin, "/exports", str(lib)], + check=True, + capture_output=True, + text=True, + ).stdout + # dumpbin output lines for export entries look like + # " 1 0 00001000 ?foo@@YAHXZ = ?foo@@YAHXZ (int __cdecl foo(void))" + # We keep all of stdout: the substring search will only fire on actual + # symbol names, headers/footers are harmless. + return raw.splitlines() + + +def _list_exports(lib: Path) -> list[str]: + if sys.platform == "darwin": + return _list_exports_macos(lib) + if sys.platform.startswith("linux"): + return _list_exports_linux(lib) + if os.name == "nt" or sys.platform == "win32": + return _list_exports_windows(lib) + sys.stderr.write(f"error: unsupported platform '{sys.platform}'\n") + sys.exit(2) + + +def main(argv: list[str]) -> int: + if len(argv) != 2: + sys.stderr.write(__doc__ or "") + return 2 + + lib = Path(argv[1]).resolve() + if not lib.exists(): + sys.stderr.write(f"error: library not found: {lib}\n") + return 2 + + forbidden = list(DEFAULT_FORBIDDEN) + extra = os.environ.get("LIVEKIT_SYMBOL_CHECK_EXTRA_FORBIDDEN", "") + if extra: + forbidden.extend(p for p in extra.split(",") if p) + + verbose = bool(os.environ.get("LIVEKIT_SYMBOL_CHECK_VERBOSE")) + + print(f"[symbol-check] library : {lib}") + print(f"[symbol-check] platform: {sys.platform}") + print(f"[symbol-check] forbidden patterns: {forbidden}") + + lines = _list_exports(lib) + print(f"[symbol-check] {len(lines)} lines of nm/dumpbin output") + + # Group leaks by pattern for a tidy summary. + leaks_by_pattern: dict[str, list[str]] = {p: [] for p in forbidden} + for line in lines: + for pat in forbidden: + if pat in line: + leaks_by_pattern[pat].append(line.rstrip()) + + total_leaks = sum(len(v) for v in leaks_by_pattern.values()) + if total_leaks == 0: + print("[symbol-check] OK: no forbidden symbols exported") + return 0 + + print(f"[symbol-check] FAIL: {total_leaks} forbidden symbol(s) exported") + for pat, hits in leaks_by_pattern.items(): + if not hits: + continue + print(f" pattern {pat!r}: {len(hits)} hit(s)") + shown = hits if verbose else hits[:MAX_REPORTED_LEAKS] + for h in shown: + print(f" {h}") + if not verbose and len(hits) > MAX_REPORTED_LEAKS: + print(f" ... and {len(hits) - MAX_REPORTED_LEAKS} more " + "(set LIVEKIT_SYMBOL_CHECK_VERBOSE=1 to see all)") + + print( + "\nliblivekit must not re-export private dependency symbols.\n" + "If you intentionally added a public symbol that triggered this, mark\n" + "it with LIVEKIT_API in include/livekit/visibility.h and rebuild.\n" + ) + return 1 + + +if __name__ == "__main__": + sys.exit(main(sys.argv)) diff --git a/.github/workflows/builds.yml b/.github/workflows/builds.yml index e677808f..e79bcde8 100644 --- a/.github/workflows/builds.yml +++ b/.github/workflows/builds.yml @@ -9,6 +9,8 @@ on: - cpp-example-collection/** - bridge/** - client-sdk-rust/** + - cmake/** + - scripts/** - CMakeLists.txt - build.sh - build.cmd @@ -27,6 +29,8 @@ on: - cpp-example-collection/** - bridge/** - client-sdk-rust/** + - cmake/** + - scripts/** - CMakeLists.txt - build.sh - build.cmd @@ -133,15 +137,14 @@ jobs: libssl-dev \ libprotobuf-dev protobuf-compiler \ libabsl-dev \ - libwayland-dev libdecor-0-dev \ - libspdlog-dev + libwayland-dev libdecor-0-dev - name: Install deps (macOS) if: runner.os == 'macOS' run: | set -eux brew update - brew install cmake ninja protobuf abseil spdlog + brew install cmake ninja protobuf abseil # ---------- Rust toolchain ---------- - name: Install Rust (stable) diff --git a/.github/workflows/make-release.yml b/.github/workflows/make-release.yml index 60909037..d1c94550 100644 --- a/.github/workflows/make-release.yml +++ b/.github/workflows/make-release.yml @@ -200,6 +200,49 @@ jobs: Write-Host "Bundle contents:" Get-ChildItem -Recurse -File $bundleDir | Select-Object -First 200 | ForEach-Object { $_.FullName } + # ---------- Verify exported ABI: no third-party symbol leaks ---------- + - name: Setup Python + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v6.0.0 + with: + python-version: '3.x' + + - name: Symbol leak check (Unix) + if: runner.os != 'Windows' + shell: bash + run: | + set -eux + libdir="build-release/lib" + if [[ "$RUNNER_OS" == "macOS" ]]; then + lib="${libdir}/liblivekit.dylib" + if [[ ! -f "${lib}" ]]; then + lib="build-release/bin/liblivekit.dylib" + fi + else + lib="${libdir}/liblivekit.so" + if [[ ! -f "${lib}" ]]; then + lib="build-release/bin/liblivekit.so" + fi + fi + python3 .github/scripts/check_no_private_symbols.py "${lib}" + + - name: Symbol leak check (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: | + $candidates = @( + "build-release/bin/livekit.dll", + "build-release/lib/livekit.dll" + ) + $lib = $null + foreach ($p in $candidates) { + if (Test-Path -LiteralPath $p) { $lib = $p; break } + } + if ($null -eq $lib) { + Write-Error "livekit.dll not found in any of: $($candidates -join ', ')" + exit 1 + } + python .github/scripts/check_no_private_symbols.py "$lib" + # ---------- Upload artifact (raw directory, no pre-compression) ---------- - name: Upload build artifact uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 5033e751..95f2b753 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -115,7 +115,6 @@ jobs: libprotobuf-dev protobuf-compiler \ libabsl-dev \ libwayland-dev libdecor-0-dev \ - libspdlog-dev \ jq - name: Install deps (macOS) @@ -123,7 +122,7 @@ jobs: run: | set -eux brew update - brew install cmake ninja protobuf abseil spdlog jq + brew install cmake ninja protobuf abseil jq # ---------- Rust toolchain ---------- - name: Install Rust (stable) diff --git a/AGENTS.md b/AGENTS.md index 0fd31fbd..366d95af 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -110,7 +110,7 @@ Updates to ./build.sh and ./build.cmd should be accompanied by updates to this f The build scripts pass an explicit job count to `cmake --build --parallel`. Set `CMAKE_BUILD_PARALLEL_LEVEL` to override the default detected logical CPU count. -**Requirements:** CMake 3.20+, C++17, Rust toolchain (cargo), protoc. On macOS: `brew install cmake ninja protobuf abseil spdlog`. On Linux: see the CI workflow for apt packages. +**Requirements:** CMake 3.20+, C++17, Rust toolchain (cargo), protoc. On macOS: `brew install cmake ninja protobuf abseil`. On Linux: see the CI workflow for apt packages. spdlog is vendored automatically via FetchContent (or vcpkg on Windows) to avoid system conflicts. ### SDK Packaging @@ -140,10 +140,70 @@ All source files must have the LiveKit Apache 2.0 copyright header. Use the curr ### Public API Surface - Keep public APIs small. Minimize what goes into `include/livekit/`. +- Use `#pragma once` to guard headers. +- All publicly visible symbols must use `LIVEKIT_API` macro (via `include/livekit/visibility.h`). - Never introduce backwards compatibility breaking changes to the public API. - `lk_log.h` lives under `src/` (internal). The public-facing logging API is `include/livekit/logging.h`. - spdlog must not appear in any public header or installed header. +### Include Conventions + +- **Public headers (`include/livekit/*.h`) must include other public headers + with the `livekit/` prefix**: `#include "livekit/track.h"`, never the bare + `#include "track.h"` form. The prefixed spelling matches what consumers see + (`#include `), resolves through the standard `-I include/` + search path rather than the implementation-defined "current directory first" + rule of `#include "..."`, and avoids accidental name collisions with + third-party headers that happen to share short names like `track.h`. +- Use double quotes for project headers (`#include "livekit/foo.h"`) and angle + brackets for system / third-party headers (`#include `, + `#include `). +- Implementation files in `src/` may include internal headers without a + prefix (`#include "ffi_client.h"`); they are not part of the public + surface and live alongside their consumers. +- Test code (in-tree or external-style canaries) must consume public + headers via `` to mirror real consumer usage + +### Symbol Visibility / Exported ABI + +The `livekit` shared library is built with hidden symbol visibility on all +platforms. Only symbols explicitly tagged with `LIVEKIT_API` (declared in +`include/livekit/visibility.h`) are part of the public ABI. Vendored static +dependencies are also compiled with hidden visibility so their symbols cannot +leak into `liblivekit.{so,dylib,dll}`. + +Rules for new code: + +- Mark any new public class, free function, or out-of-line static method with + `LIVEKIT_API`. POD/aggregate structs and pure-inline classes do not need + annotation because they emit no out-of-line symbols. +- For internal symbols that must remain visible to in-tree tests (the tests + link the same shared library), use `LIVEKIT_INTERNAL_API`. External + consumers must not rely on these. +- Do not add `__declspec(dllexport)` or `__attribute__((visibility("default")))` + by hand; always go through `LIVEKIT_API` / `LIVEKIT_INTERNAL_API`. +- On Windows, `WINDOWS_EXPORT_ALL_SYMBOLS` is **deliberately disabled** for the + `livekit` target. Use `LIVEKIT_API` to export. + +The exported ABI is enforced by `.github/scripts/check_no_private_symbols.py`, +run from the `make-release.yml` "Symbol leak check" CI step so a leak blocks +the release build itself (it does not run on regular pushes/PRs). The script +fails if `nm`/`dumpbin` reports any exported symbol matching a forbidden +substring (currently `spdlog::`, `fmt::v`, `google::protobuf`, `absl::`). To +run it locally, point it at the built shared library: + +```bash +python3 .github/scripts/check_no_private_symbols.py \ + build-release/lib/liblivekit.dylib # or .so / livekit.dll +``` + +**When adding a new third-party library or vendored dependency to the SDK, +update `.github/scripts/check_no_private_symbols.py` to add a forbidden +substring pattern for the new dependency's namespace/symbol prefix.** The +denylist is intentionally explicit — a new dep that leaks symbols will +otherwise silently pollute `liblivekit.{so,dylib,dll}` and clash at runtime +with the same library loaded elsewhere in the host process. + ### Error Handling - Use `LK_LOG_WARN` for non-fatal unexpected conditions. @@ -167,11 +227,13 @@ All source files must have the LiveKit Apache 2.0 copyright header. Use the curr - The CMake install produces a `find_package(LiveKit CONFIG)`-compatible package with `LiveKitConfig.cmake`, `LiveKitTargets.cmake`, and `LiveKitConfigVersion.cmake`. ### Readability and Performance + Code should be easy to read and understand. If a sacrifice is made for performance or readability, it should be documented. -Adhere to clang-format checks configured in `.clang-format`. Run `./scripts/clang-format.sh` if needed to confirm code styling. +Adhere to clang-format checks configured in `.clang-format`. Run `./scripts/clang-format.sh` if needed to confirm code styling, and `./scripts/clang-format.sh --fix` to auto-format generated code created. ### Static Analysis + Adhere to clang-tidy checks configured in `.clang-tidy`. Run `./scripts/clang-tidy.sh` if needed to confirm code quality. ## Dependencies @@ -183,6 +245,12 @@ Adhere to clang-tidy checks configured in `.clang-tidy`. Run `./scripts/clang-ti | client-sdk-rust | Build-time | Git submodule, built via cargo during CMake build | | Google Test | Test only | FetchContent in `src/tests/CMakeLists.txt` | +When adding a new private/vendored dependency to this table, also add a +forbidden symbol pattern for it to +`.github/scripts/check_no_private_symbols.py` so the "Symbol leak check" +CI step will fail loudly if its symbols escape the public ABI of +`liblivekit`. + ## Testing Tests are under `src/tests/` using Google Test: @@ -197,10 +265,8 @@ Integration tests (`src/tests/integration/`) cover: room connections, callbacks, When adding new client facing functionality, add a new test case to the existing test suite. When adding new client facing functionality, add benchmarking to understand the limitations of the new functionality. -## Formatting -Use clang formatting that aligns with the existing codebase. - ## General C++ Development + - Do not use dynamic memory allocation after initialization - Keep each function short (roughly ≤ 60 lines) - Declare all data objects at the smallest possible level of scope diff --git a/CMakeLists.txt b/CMakeLists.txt index 6a1946b7..3f71c956 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -96,6 +96,13 @@ endif() message(STATUS "Using protoc: ${Protobuf_PROTOC_EXECUTABLE}") add_library(livekit_proto OBJECT ${FFI_PROTO_FILES}) +# livekit_proto is compiled into liblivekit; apply the same hidden visibility +# policy so generated protobuf code does not leak into the SDK's exported ABI. +set_target_properties(livekit_proto PROPERTIES + CXX_VISIBILITY_PRESET hidden + C_VISIBILITY_PRESET hidden + VISIBILITY_INLINES_HIDDEN ON +) if(TARGET protobuf::libprotobuf) set(LIVEKIT_PROTOBUF_TARGET protobuf::libprotobuf) else() @@ -366,8 +373,23 @@ add_library(livekit SHARED src/trace/trace_event.h src/trace/tracing.cpp ) -if(WIN32) - set_target_properties(livekit PROPERTIES WINDOWS_EXPORT_ALL_SYMBOLS ON) +# Symbol visibility: keep liblivekit's exported ABI restricted to the public +# LiveKit API. By default everything is hidden unless marked by the LIVEKIT_API +# macro in code +set_target_properties(livekit PROPERTIES + CXX_VISIBILITY_PRESET hidden + C_VISIBILITY_PRESET hidden + VISIBILITY_INLINES_HIDDEN ON +) +target_compile_definitions(livekit PRIVATE LIVEKIT_BUILDING_SDK=1) + +# Linker-level safe guard for Linux: even if a static-archive object +# slipped through with default visibility, --exclude-libs,ALL strips it from +# the dynamic symbol table. ld64 (macOS) has no equivalent flag; hidden +# visibility per TU is sufficient there. On Windows, explicit dllexport via +# LIVEKIT_API replaces WINDOWS_EXPORT_ALL_SYMBOLS +if(UNIX AND NOT APPLE) + target_link_options(livekit PRIVATE "LINKER:--exclude-libs,ALL") endif() diff --git a/bridge/CMakeLists.txt b/bridge/CMakeLists.txt index 549e86cb..3b5e9f90 100644 --- a/bridge/CMakeLists.txt +++ b/bridge/CMakeLists.txt @@ -14,8 +14,20 @@ add_library(livekit_bridge SHARED src/rpc_controller.h ) +# Workaround for bridge deprecation: handle visibility differently to avoid +# major LIVEKIT_API changes across its code, and not affecting the SDK if(WIN32) set_target_properties(livekit_bridge PROPERTIES WINDOWS_EXPORT_ALL_SYMBOLS ON) +else() + set_target_properties(livekit_bridge PROPERTIES + CXX_VISIBILITY_PRESET default + C_VISIBILITY_PRESET default + VISIBILITY_INLINES_HIDDEN OFF + ) + target_compile_options(livekit_bridge INTERFACE + -fvisibility=default + -fno-visibility-inlines-hidden + ) endif() target_include_directories(livekit_bridge diff --git a/cmake/protobuf.cmake b/cmake/protobuf.cmake index 42901db3..8132a084 100644 --- a/cmake/protobuf.cmake +++ b/cmake/protobuf.cmake @@ -119,6 +119,18 @@ set(protobuf_INSTALL OFF CACHE BOOL "" FORCE) set(ABSL_ENABLE_INSTALL OFF CACHE BOOL "" FORCE) set(utf8_range_ENABLE_INSTALL OFF CACHE BOOL "" FORCE) +# Force hidden visibility on every target created by the FetchContent +# subprojects below. Setting these as CACHE BOOL FORCE before +# FetchContent_MakeAvailable propagates to every add_library() call inside +# absl / protobuf / utf8_range, so their .o files are compiled with +# -fvisibility=hidden -fvisibility-inlines-hidden. Without this, even though +# liblivekit itself is hidden, absl's static archives would carry default- +# visibility symbols that the linker happily re-exports. +set(CMAKE_CXX_VISIBILITY_PRESET hidden CACHE STRING "" FORCE) +set(CMAKE_C_VISIBILITY_PRESET hidden CACHE STRING "" FORCE) +set(CMAKE_VISIBILITY_INLINES_HIDDEN ON CACHE BOOL "" FORCE) +set(CMAKE_POSITION_INDEPENDENT_CODE ON CACHE BOOL "" FORCE) + set(protobuf_BUILD_TESTS OFF CACHE BOOL "" FORCE) set(protobuf_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) set(protobuf_BUILD_CONFORMANCE OFF CACHE BOOL "" FORCE) diff --git a/cmake/spdlog.cmake b/cmake/spdlog.cmake index b0956b00..0ef0e191 100644 --- a/cmake/spdlog.cmake +++ b/cmake/spdlog.cmake @@ -50,6 +50,12 @@ include(FetchContent) set(LIVEKIT_SPDLOG_VERSION "1.15.1" CACHE STRING "Vendored spdlog version") +# Warn any existing users of old option +if(DEFINED LIVEKIT_USE_SYSTEM_SPDLOG) + message(WARNING + "LIVEKIT_USE_SYSTEM_SPDLOG is no longer supported and will be ignored") +endif() + # --------------------------------------------------------------------------- # Windows: use vcpkg # --------------------------------------------------------------------------- @@ -75,4 +81,16 @@ set(SPDLOG_INSTALL OFF CACHE BOOL "" FORCE) FetchContent_MakeAvailable(livekit_spdlog) +# spdlog is linked PRIVATE into liblivekit and must not leak its symbols into +# the SDK's exported ABI. Force hidden visibility on the spdlog target so its +# object files don't carry default-visibility symbols into liblivekit. +if(TARGET spdlog) + set_target_properties(spdlog PROPERTIES + CXX_VISIBILITY_PRESET hidden + C_VISIBILITY_PRESET hidden + VISIBILITY_INLINES_HIDDEN ON + POSITION_INDEPENDENT_CODE ON + ) +endif() + message(STATUS "macOS/Linux: using vendored spdlog v${LIVEKIT_SPDLOG_VERSION}") diff --git a/docker/Dockerfile.base b/docker/Dockerfile.base index 12b83721..54559254 100644 --- a/docker/Dockerfile.base +++ b/docker/Dockerfile.base @@ -34,7 +34,6 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ libglib2.0-dev \ libprotobuf-dev \ libdecor-0-dev \ - libspdlog-dev \ libssl-dev \ libunwind-dev \ libusb-1.0-0-dev \ diff --git a/include/livekit/audio_frame.h b/include/livekit/audio_frame.h index 139804df..1a28d4d0 100644 --- a/include/livekit/audio_frame.h +++ b/include/livekit/audio_frame.h @@ -20,6 +20,8 @@ #include #include +#include "livekit/visibility.h" + namespace livekit { namespace proto { @@ -34,7 +36,7 @@ class OwnedAudioFrameBuffer; * number of channels, and samples per channel. It is used for capturing and * processing audio in the LiveKit SDK. */ -class AudioFrame { +class LIVEKIT_API AudioFrame { public: /** * Construct an AudioFrame from raw PCM samples. diff --git a/include/livekit/audio_processing_module.h b/include/livekit/audio_processing_module.h index 5f5f204c..ba37cc7b 100644 --- a/include/livekit/audio_processing_module.h +++ b/include/livekit/audio_processing_module.h @@ -20,6 +20,7 @@ #include "livekit/audio_frame.h" #include "livekit/ffi_handle.h" +#include "livekit/visibility.h" namespace livekit { @@ -41,7 +42,7 @@ namespace livekit { * * Note: Audio frames must be exactly 10ms in duration. */ -class AudioProcessingModule { +class LIVEKIT_API AudioProcessingModule { public: /** * @brief Configuration options for the Audio Processing Module. diff --git a/include/livekit/audio_source.h b/include/livekit/audio_source.h index e5d1d5e0..d8c0fd6c 100644 --- a/include/livekit/audio_source.h +++ b/include/livekit/audio_source.h @@ -20,6 +20,7 @@ #include "livekit/audio_frame.h" #include "livekit/ffi_handle.h" +#include "livekit/visibility.h" namespace livekit { @@ -33,7 +34,7 @@ class FfiClient; /** * Represents a real-time audio source with an internal audio queue. */ -class AudioSource { +class LIVEKIT_API AudioSource { public: /** * Create a new native audio source. diff --git a/include/livekit/audio_stream.h b/include/livekit/audio_stream.h index c7dba834..57be0a89 100644 --- a/include/livekit/audio_stream.h +++ b/include/livekit/audio_stream.h @@ -24,10 +24,11 @@ #include #include -#include "audio_frame.h" -#include "ffi_handle.h" -#include "participant.h" -#include "track.h" +#include "livekit/audio_frame.h" +#include "livekit/ffi_handle.h" +#include "livekit/participant.h" +#include "livekit/track.h" +#include "livekit/visibility.h" namespace livekit { @@ -65,7 +66,7 @@ struct AudioFrameEvent { * * stream->close(); // optional, called automatically in destructor */ -class AudioStream { +class LIVEKIT_API AudioStream { public: /// Configuration options for AudioStream creation. struct Options { diff --git a/include/livekit/data_stream.h b/include/livekit/data_stream.h index ddd65c67..ed81cf83 100644 --- a/include/livekit/data_stream.h +++ b/include/livekit/data_stream.h @@ -27,6 +27,8 @@ #include #include +#include "livekit/visibility.h" + namespace livekit { class LocalParticipant; @@ -75,7 +77,7 @@ struct ByteStreamInfo : BaseStreamInfo { /// Reader for incoming text streams. /// Created internally by the SDK when a text stream header is received. -class TextStreamReader { +class LIVEKIT_API TextStreamReader { public: /// Construct a reader from initial stream metadata. explicit TextStreamReader(TextStreamInfo info); @@ -115,7 +117,7 @@ class TextStreamReader { }; /// Reader for incoming byte streams. -class ByteStreamReader { +class LIVEKIT_API ByteStreamReader { public: /// Construct a reader from initial stream metadata. explicit ByteStreamReader(ByteStreamInfo info); @@ -151,7 +153,7 @@ class ByteStreamReader { /// Base class for sending data streams. /// Concrete subclasses are TextStreamWriter and ByteStreamWriter. -class BaseStreamWriter { +class LIVEKIT_API BaseStreamWriter { public: virtual ~BaseStreamWriter() = default; @@ -215,7 +217,7 @@ class BaseStreamWriter { }; /// Writer for outgoing text streams. -class TextStreamWriter : public BaseStreamWriter { +class LIVEKIT_API TextStreamWriter : public BaseStreamWriter { public: TextStreamWriter(LocalParticipant& local_participant, const std::string& topic = "", const std::map& attributes = {}, const std::string& stream_id = "", @@ -237,7 +239,7 @@ class TextStreamWriter : public BaseStreamWriter { }; /// Writer for outgoing byte streams. -class ByteStreamWriter : public BaseStreamWriter { +class LIVEKIT_API ByteStreamWriter : public BaseStreamWriter { public: ByteStreamWriter(LocalParticipant& local_participant, const std::string& name, const std::string& topic = "", const std::map& attributes = {}, const std::string& stream_id = "", diff --git a/include/livekit/data_track_error.h b/include/livekit/data_track_error.h index e8519a64..5047a8a1 100644 --- a/include/livekit/data_track_error.h +++ b/include/livekit/data_track_error.h @@ -20,6 +20,8 @@ #include #include +#include "livekit/visibility.h" + namespace livekit { namespace proto { @@ -45,7 +47,7 @@ struct PublishDataTrackError { PublishDataTrackErrorCode code{PublishDataTrackErrorCode::UNKNOWN}; std::string message; - static PublishDataTrackError fromProto(const proto::PublishDataTrackError& error); + LIVEKIT_API static PublishDataTrackError fromProto(const proto::PublishDataTrackError& error); }; enum class LocalDataTrackTryPushErrorCode : std::uint32_t { @@ -60,7 +62,7 @@ struct LocalDataTrackTryPushError { LocalDataTrackTryPushErrorCode code{LocalDataTrackTryPushErrorCode::UNKNOWN}; std::string message; - static LocalDataTrackTryPushError fromProto(const proto::LocalDataTrackTryPushError& error); + LIVEKIT_API static LocalDataTrackTryPushError fromProto(const proto::LocalDataTrackTryPushError& error); }; enum class SubscribeDataTrackErrorCode : std::uint32_t { @@ -77,7 +79,7 @@ struct SubscribeDataTrackError { SubscribeDataTrackErrorCode code{SubscribeDataTrackErrorCode::UNKNOWN}; std::string message; - static SubscribeDataTrackError fromProto(const proto::SubscribeDataTrackError& error); + LIVEKIT_API static SubscribeDataTrackError fromProto(const proto::SubscribeDataTrackError& error); }; } // namespace livekit diff --git a/include/livekit/data_track_frame.h b/include/livekit/data_track_frame.h index fa37c490..1e82ac5a 100644 --- a/include/livekit/data_track_frame.h +++ b/include/livekit/data_track_frame.h @@ -20,6 +20,8 @@ #include #include +#include "livekit/visibility.h" + namespace livekit { namespace proto { @@ -60,7 +62,7 @@ struct DataTrackFrame { * @param owned The proto::DataTrackFrame to create a DataTrackFrame from. * @return The created DataTrackFrame. */ - static DataTrackFrame fromOwnedInfo(const proto::DataTrackFrame& owned); + LIVEKIT_API static DataTrackFrame fromOwnedInfo(const proto::DataTrackFrame& owned); }; } // namespace livekit diff --git a/include/livekit/data_track_stream.h b/include/livekit/data_track_stream.h index 1f243de3..5c8c3cc3 100644 --- a/include/livekit/data_track_stream.h +++ b/include/livekit/data_track_stream.h @@ -26,6 +26,7 @@ #include "livekit/data_track_error.h" #include "livekit/data_track_frame.h" #include "livekit/ffi_handle.h" +#include "livekit/visibility.h" namespace livekit { @@ -53,7 +54,7 @@ class FfiEvent; * } * } */ -class DataTrackStream { +class LIVEKIT_API DataTrackStream { public: struct Options { /// Maximum frames buffered on the Rust side. Rust defaults to 16. diff --git a/include/livekit/e2ee.h b/include/livekit/e2ee.h index 1260aaf2..dfd55a60 100644 --- a/include/livekit/e2ee.h +++ b/include/livekit/e2ee.h @@ -22,6 +22,8 @@ #include #include +#include "livekit/visibility.h" + namespace livekit { /* Encryption algorithm type used by the underlying stack. @@ -105,7 +107,7 @@ struct E2EEOptions { * - Providing a shared key up-front is convenient for shared-key E2EE, but is * not required by the API shape (keys may be supplied later). */ -class E2EEManager { +class LIVEKIT_API E2EEManager { public: /** If your application requires key rotation during the lifetime of a single * room or unique keys per participant (such as when implementing the MEGOLM @@ -113,7 +115,7 @@ class E2EEManager { * https://docs.livekit.io/home/client/encryption/#custom-key-provider doe * details * */ - class KeyProvider { + class LIVEKIT_API KeyProvider { public: ~KeyProvider() = default; @@ -150,7 +152,7 @@ class E2EEManager { KeyProviderOptions options_; }; - class FrameCryptor { + class LIVEKIT_API FrameCryptor { public: FrameCryptor(std::uint64_t room_handle, std::string participant_identity, int key_index, bool enabled); ~FrameCryptor() = default; diff --git a/include/livekit/export.h b/include/livekit/export.h new file mode 100644 index 00000000..c7921fb1 --- /dev/null +++ b/include/livekit/export.h @@ -0,0 +1,63 @@ +/* + * Copyright 2026 LiveKit + * + * 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. + */ + +#pragma once + +// LIVEKIT_API marks a symbol as part of the public ABI of liblivekit. +// +// On Unix, the SDK is built with -fvisibility=hidden / -fvisibility-inlines-hidden, +// so every symbol defaults to hidden. LIVEKIT_API re-exposes the symbol with +// default visibility. Consumers see no annotation (just a normal declaration). +// +// On Windows, the SDK is built without WINDOWS_EXPORT_ALL_SYMBOLS, so symbols +// must be explicitly tagged with __declspec(dllexport) when building the SDK +// and __declspec(dllimport) when consuming it. LIVEKIT_BUILDING_SDK is defined +// only when compiling the SDK itself (set in CMakeLists.txt). + +#if defined(_WIN32) +#if defined(LIVEKIT_BUILDING_SDK) +#define LIVEKIT_API __declspec(dllexport) +#else +#define LIVEKIT_API __declspec(dllimport) +#endif +#else +#if defined(LIVEKIT_BUILDING_SDK) +#define LIVEKIT_API __attribute__((visibility("default"))) +#else +#define LIVEKIT_API +#endif +#endif + +// LIVEKIT_INTERNAL_API marks a symbol that is NOT part of the public ABI but +// must remain visible so that the in-tree test binaries (which link against +// the same shared library) can resolve it. +// +// External consumers must not rely on LIVEKIT_INTERNAL_API symbols; they may +// change or disappear without notice. +// +// On Windows, internal symbols are exported the same way as public ones +// because tests link via the standard import library; on Unix, hidden +// visibility is overridden for these specific symbols only. + +#if defined(_WIN32) +#define LIVEKIT_INTERNAL_API LIVEKIT_API +#else +#if defined(LIVEKIT_BUILDING_SDK) +#define LIVEKIT_INTERNAL_API __attribute__((visibility("default"))) +#else +#define LIVEKIT_INTERNAL_API +#endif +#endif diff --git a/include/livekit/ffi_handle.h b/include/livekit/ffi_handle.h index 7cd10466..840aa384 100644 --- a/include/livekit/ffi_handle.h +++ b/include/livekit/ffi_handle.h @@ -18,6 +18,8 @@ #include +#include "livekit/visibility.h" + namespace livekit { /** @@ -26,7 +28,7 @@ namespace livekit { * Ensures that the handle is automatically released via * livekit_ffi_drop_handle() when the object goes out of scope. */ -class FfiHandle { +class LIVEKIT_API FfiHandle { public: explicit FfiHandle(uintptr_t h = 0) noexcept; ~FfiHandle(); diff --git a/include/livekit/livekit.h b/include/livekit/livekit.h index fb668611..f7906c42 100644 --- a/include/livekit/livekit.h +++ b/include/livekit/livekit.h @@ -16,28 +16,29 @@ #pragma once -#include "audio_frame.h" -#include "audio_processing_module.h" -#include "audio_source.h" -#include "audio_stream.h" -#include "build.h" -#include "e2ee.h" -#include "local_audio_track.h" -#include "local_participant.h" -#include "local_track_publication.h" -#include "local_video_track.h" -#include "logging.h" -#include "participant.h" -#include "remote_participant.h" -#include "remote_track_publication.h" -#include "room.h" -#include "room_delegate.h" -#include "room_event_types.h" -#include "tracing.h" -#include "track_publication.h" -#include "video_frame.h" -#include "video_source.h" -#include "video_stream.h" +#include "livekit/audio_frame.h" +#include "livekit/audio_processing_module.h" +#include "livekit/audio_source.h" +#include "livekit/audio_stream.h" +#include "livekit/build.h" +#include "livekit/e2ee.h" +#include "livekit/local_audio_track.h" +#include "livekit/local_participant.h" +#include "livekit/local_track_publication.h" +#include "livekit/local_video_track.h" +#include "livekit/logging.h" +#include "livekit/participant.h" +#include "livekit/remote_participant.h" +#include "livekit/remote_track_publication.h" +#include "livekit/room.h" +#include "livekit/room_delegate.h" +#include "livekit/room_event_types.h" +#include "livekit/tracing.h" +#include "livekit/track_publication.h" +#include "livekit/video_frame.h" +#include "livekit/video_source.h" +#include "livekit/video_stream.h" +#include "livekit/visibility.h" namespace livekit { @@ -59,11 +60,11 @@ enum class LogSink { /// @param log_sink The log sink to use for SDK messages (default: Console). /// @returns true if initialization happened on this call, false if it was /// already initialized. -bool initialize(const LogLevel& level = LogLevel::Info, const LogSink& log_sink = LogSink::kConsole); +LIVEKIT_API bool initialize(const LogLevel& level = LogLevel::Info, const LogSink& log_sink = LogSink::kConsole); /// Shut down the LiveKit SDK. /// /// After shutdown, you may call initialize() again. -void shutdown(); +LIVEKIT_API void shutdown(); } // namespace livekit \ No newline at end of file diff --git a/include/livekit/local_audio_track.h b/include/livekit/local_audio_track.h index 72359efa..53823f7b 100644 --- a/include/livekit/local_audio_track.h +++ b/include/livekit/local_audio_track.h @@ -19,9 +19,10 @@ #include #include -#include "audio_frame.h" -#include "local_track_publication.h" -#include "track.h" +#include "livekit/audio_frame.h" +#include "livekit/local_track_publication.h" +#include "livekit/track.h" +#include "livekit/visibility.h" namespace livekit { @@ -51,7 +52,7 @@ class AudioSource; * The track name provided during creation is visible to remote * participants and can be used for debugging or UI display. */ -class LocalAudioTrack : public Track { +class LIVEKIT_API LocalAudioTrack : public Track { public: /// Creates a new local audio track backed by the given `AudioSource`. /// diff --git a/include/livekit/local_data_track.h b/include/livekit/local_data_track.h index 1e4365d7..45e38611 100644 --- a/include/livekit/local_data_track.h +++ b/include/livekit/local_data_track.h @@ -27,6 +27,7 @@ #include "livekit/data_track_info.h" #include "livekit/ffi_handle.h" #include "livekit/result.h" +#include "livekit/visibility.h" namespace livekit { @@ -55,7 +56,7 @@ class OwnedLocalDataTrack; * dt->unpublishDataTrack(); * } */ -class LocalDataTrack { +class LIVEKIT_API LocalDataTrack { public: ~LocalDataTrack() = default; diff --git a/include/livekit/local_participant.h b/include/livekit/local_participant.h index 52253d7e..bcb94bd7 100644 --- a/include/livekit/local_participant.h +++ b/include/livekit/local_participant.h @@ -33,6 +33,7 @@ #include "livekit/participant.h" #include "livekit/room_event_types.h" #include "livekit/rpc_error.h" +#include "livekit/visibility.h" namespace livekit { @@ -54,7 +55,7 @@ struct RpcInvocationData { * * LocalParticipant, built on top of the participant.h base class. */ -class LocalParticipant : public Participant { +class LIVEKIT_API LocalParticipant : public Participant { public: using PublicationMap = std::unordered_map>; using TrackMap = std::unordered_map>; diff --git a/include/livekit/local_track_publication.h b/include/livekit/local_track_publication.h index 51d1820a..50f5fd74 100644 --- a/include/livekit/local_track_publication.h +++ b/include/livekit/local_track_publication.h @@ -17,6 +17,7 @@ #pragma once #include "livekit/track_publication.h" +#include "livekit/visibility.h" namespace livekit { @@ -24,7 +25,7 @@ namespace proto { class OwnedTrackPublication; } -class LocalTrackPublication : public TrackPublication { +class LIVEKIT_API LocalTrackPublication : public TrackPublication { public: /// Note, this LocalTrackPublication is constructed internally only; /// safe to accept proto::OwnedTrackPublication. diff --git a/include/livekit/local_video_track.h b/include/livekit/local_video_track.h index 7fc316b8..98b0b801 100644 --- a/include/livekit/local_video_track.h +++ b/include/livekit/local_video_track.h @@ -19,8 +19,9 @@ #include #include -#include "local_track_publication.h" -#include "track.h" +#include "livekit/local_track_publication.h" +#include "livekit/track.h" +#include "livekit/visibility.h" namespace livekit { @@ -51,7 +52,7 @@ class VideoSource; * The track name provided during creation is visible to remote * participants and can be used for debugging or UI display. */ -class LocalVideoTrack : public Track { +class LIVEKIT_API LocalVideoTrack : public Track { public: /// Creates a new local video track backed by the given `VideoSource`. /// diff --git a/include/livekit/logging.h b/include/livekit/logging.h index 4ba93401..4e5a145d 100644 --- a/include/livekit/logging.h +++ b/include/livekit/logging.h @@ -19,6 +19,8 @@ #include #include +#include "livekit/visibility.h" + namespace livekit { /// Severity levels for SDK log messages. @@ -36,10 +38,10 @@ enum class LogLevel { /// /// Messages below this level are discarded before reaching any sink /// or callback. Thread-safe; may be called at any time after initialize(). -void setLogLevel(LogLevel level); +LIVEKIT_API void setLogLevel(LogLevel level); /// Return the current minimum log level. -LogLevel getLogLevel(); +LIVEKIT_API LogLevel getLogLevel(); /// Signature for a user-supplied log callback. /// @@ -56,6 +58,6 @@ using LogCallback = std::function attributes, ParticipantKind kind, DisconnectReason reason) diff --git a/include/livekit/remote_audio_track.h b/include/livekit/remote_audio_track.h index cfe8761c..060b5825 100644 --- a/include/livekit/remote_audio_track.h +++ b/include/livekit/remote_audio_track.h @@ -19,7 +19,8 @@ #include #include -#include "track.h" +#include "livekit/track.h" +#include "livekit/visibility.h" namespace livekit { @@ -41,7 +42,7 @@ class AudioSource; * Applications generally interact with `RemoteAudioTrack` through events and * `RemoteTrackPublication`, not through direct construction. */ -class RemoteAudioTrack : public Track { +class LIVEKIT_API RemoteAudioTrack : public Track { public: /// Constructs a `RemoteAudioTrack` from an internal protocol-level /// `OwnedTrack` description provided by the signaling/FFI layer. diff --git a/include/livekit/remote_data_track.h b/include/livekit/remote_data_track.h index a0b40515..965893ca 100644 --- a/include/livekit/remote_data_track.h +++ b/include/livekit/remote_data_track.h @@ -24,6 +24,7 @@ #include "livekit/data_track_stream.h" #include "livekit/ffi_handle.h" #include "livekit/result.h" +#include "livekit/visibility.h" namespace livekit { @@ -64,7 +65,7 @@ class RemoteDataTrack { const std::string& publisherIdentity() const noexcept { return publisher_identity_; } /// Whether the track is still published by the remote participant. - bool isPublished() const; + LIVEKIT_API bool isPublished() const; #ifdef LIVEKIT_TEST_ACCESS /// Test-only accessor for exercising lower-level FFI subscription paths. @@ -77,7 +78,7 @@ class RemoteDataTrack { * Returns a DataTrackStream that delivers frames via blocking * read(). Destroy the stream to unsubscribe. */ - Result, SubscribeDataTrackError> subscribe( + LIVEKIT_API Result, SubscribeDataTrackError> subscribe( const DataTrackStream::Options& options = {}); private: diff --git a/include/livekit/remote_participant.h b/include/livekit/remote_participant.h index 938086a0..67f5e9a4 100644 --- a/include/livekit/remote_participant.h +++ b/include/livekit/remote_participant.h @@ -20,13 +20,14 @@ #include #include -#include "participant.h" +#include "livekit/participant.h" +#include "livekit/visibility.h" namespace livekit { class RemoteTrackPublication; -class RemoteParticipant : public Participant { +class LIVEKIT_API RemoteParticipant : public Participant { public: using PublicationMap = std::unordered_map>; @@ -53,6 +54,6 @@ class RemoteParticipant : public Participant { }; // Convenience for logging / streaming -std::ostream& operator<<(std::ostream& os, const RemoteParticipant& participant); +LIVEKIT_API std::ostream& operator<<(std::ostream& os, const RemoteParticipant& participant); } // namespace livekit diff --git a/include/livekit/remote_track_publication.h b/include/livekit/remote_track_publication.h index 5ad4c0fe..2568a55d 100644 --- a/include/livekit/remote_track_publication.h +++ b/include/livekit/remote_track_publication.h @@ -17,6 +17,7 @@ #pragma once #include "livekit/track_publication.h" +#include "livekit/visibility.h" namespace livekit { @@ -24,7 +25,7 @@ namespace proto { class OwnedTrackPublication; } -class RemoteTrackPublication : public TrackPublication { +class LIVEKIT_API RemoteTrackPublication : public TrackPublication { public: /// Note, this RemoteTrackPublication is constructed internally only; /// safe to accept proto::OwnedTrackPublication. diff --git a/include/livekit/remote_video_track.h b/include/livekit/remote_video_track.h index 891d8fbd..1df57220 100644 --- a/include/livekit/remote_video_track.h +++ b/include/livekit/remote_video_track.h @@ -19,7 +19,8 @@ #include #include -#include "track.h" +#include "livekit/track.h" +#include "livekit/visibility.h" namespace livekit { @@ -41,7 +42,7 @@ class VideoSource; * Applications generally interact with `RemoteVideoTrack` through events and * `RemoteTrackPublication`, not through direct construction. */ -class RemoteVideoTrack : public Track { +class LIVEKIT_API RemoteVideoTrack : public Track { public: /// Constructs a `RemoteVideoTrack` from an internal protocol-level /// `OwnedTrack` description provided by the signaling/FFI layer. diff --git a/include/livekit/room.h b/include/livekit/room.h index 932e01cd..80b23aaa 100644 --- a/include/livekit/room.h +++ b/include/livekit/room.h @@ -26,6 +26,7 @@ #include "livekit/ffi_handle.h" #include "livekit/room_event_types.h" #include "livekit/subscription_thread_dispatcher.h" +#include "livekit/visibility.h" namespace livekit { @@ -94,7 +95,7 @@ struct RoomOptions { /// - participant list (local + remote) /// - track publications /// - server events forwarded to a RoomDelegate -class Room { +class LIVEKIT_API Room { public: Room(); ~Room(); diff --git a/include/livekit/room_delegate.h b/include/livekit/room_delegate.h index addc90c4..79c484aa 100644 --- a/include/livekit/room_delegate.h +++ b/include/livekit/room_delegate.h @@ -17,6 +17,7 @@ #pragma once #include "livekit/room_event_types.h" +#include "livekit/visibility.h" namespace livekit { @@ -31,7 +32,7 @@ class Room; * All methods provide default no-op implementations so you can override * only the callbacks you care about. */ -class RoomDelegate { +class LIVEKIT_API RoomDelegate { public: virtual ~RoomDelegate() = default; diff --git a/include/livekit/rpc_error.h b/include/livekit/rpc_error.h index 3756907e..9e449644 100644 --- a/include/livekit/rpc_error.h +++ b/include/livekit/rpc_error.h @@ -20,6 +20,8 @@ #include #include +#include "livekit/visibility.h" + namespace livekit { namespace proto { @@ -37,7 +39,7 @@ class RpcError; * Built-in errors are included (codes 1400–1999) but developers may use * arbitrary codes as well. */ -class RpcError : public std::runtime_error { +class LIVEKIT_API RpcError : public std::runtime_error { public: /** * Built-in error codes diff --git a/include/livekit/stats.h b/include/livekit/stats.h index 70a526f8..84674d8e 100644 --- a/include/livekit/stats.h +++ b/include/livekit/stats.h @@ -23,6 +23,8 @@ #include #include +#include "livekit/visibility.h" + namespace livekit { namespace proto { @@ -503,32 +505,32 @@ struct RtcStats { // fromProto declarations // ---------------------- -RtcStatsData fromProto(const proto::RtcStatsData&); - -CodecStats fromProto(const proto::CodecStats&); -RtpStreamStats fromProto(const proto::RtpStreamStats&); -ReceivedRtpStreamStats fromProto(const proto::ReceivedRtpStreamStats&); -InboundRtpStreamStats fromProto(const proto::InboundRtpStreamStats&); -SentRtpStreamStats fromProto(const proto::SentRtpStreamStats&); -OutboundRtpStreamStats fromProto(const proto::OutboundRtpStreamStats&); -RemoteInboundRtpStreamStats fromProto(const proto::RemoteInboundRtpStreamStats&); -RemoteOutboundRtpStreamStats fromProto(const proto::RemoteOutboundRtpStreamStats&); -MediaSourceStats fromProto(const proto::MediaSourceStats&); -AudioSourceStats fromProto(const proto::AudioSourceStats&); -VideoSourceStats fromProto(const proto::VideoSourceStats&); -AudioPlayoutStats fromProto(const proto::AudioPlayoutStats&); -PeerConnectionStats fromProto(const proto::PeerConnectionStats&); -DataChannelStats fromProto(const proto::DataChannelStats&); -TransportStats fromProto(const proto::TransportStats&); -CandidatePairStats fromProto(const proto::CandidatePairStats&); -IceCandidateStats fromProto(const proto::IceCandidateStats&); -CertificateStats fromProto(const proto::CertificateStats&); -StreamStats fromProto(const proto::StreamStats&); +LIVEKIT_API RtcStatsData fromProto(const proto::RtcStatsData&); + +LIVEKIT_API CodecStats fromProto(const proto::CodecStats&); +LIVEKIT_API RtpStreamStats fromProto(const proto::RtpStreamStats&); +LIVEKIT_API ReceivedRtpStreamStats fromProto(const proto::ReceivedRtpStreamStats&); +LIVEKIT_API InboundRtpStreamStats fromProto(const proto::InboundRtpStreamStats&); +LIVEKIT_API SentRtpStreamStats fromProto(const proto::SentRtpStreamStats&); +LIVEKIT_API OutboundRtpStreamStats fromProto(const proto::OutboundRtpStreamStats&); +LIVEKIT_API RemoteInboundRtpStreamStats fromProto(const proto::RemoteInboundRtpStreamStats&); +LIVEKIT_API RemoteOutboundRtpStreamStats fromProto(const proto::RemoteOutboundRtpStreamStats&); +LIVEKIT_API MediaSourceStats fromProto(const proto::MediaSourceStats&); +LIVEKIT_API AudioSourceStats fromProto(const proto::AudioSourceStats&); +LIVEKIT_API VideoSourceStats fromProto(const proto::VideoSourceStats&); +LIVEKIT_API AudioPlayoutStats fromProto(const proto::AudioPlayoutStats&); +LIVEKIT_API PeerConnectionStats fromProto(const proto::PeerConnectionStats&); +LIVEKIT_API DataChannelStats fromProto(const proto::DataChannelStats&); +LIVEKIT_API TransportStats fromProto(const proto::TransportStats&); +LIVEKIT_API CandidatePairStats fromProto(const proto::CandidatePairStats&); +LIVEKIT_API IceCandidateStats fromProto(const proto::IceCandidateStats&); +LIVEKIT_API CertificateStats fromProto(const proto::CertificateStats&); +LIVEKIT_API StreamStats fromProto(const proto::StreamStats&); // High-level: -RtcStats fromProto(const proto::RtcStats&); +LIVEKIT_API RtcStats fromProto(const proto::RtcStats&); // helper if you have repeated RtcStats in proto: -std::vector fromProto(const std::vector&); +LIVEKIT_API std::vector fromProto(const std::vector&); } // namespace livekit diff --git a/include/livekit/subscription_thread_dispatcher.h b/include/livekit/subscription_thread_dispatcher.h index dfe53d32..bdd41618 100644 --- a/include/livekit/subscription_thread_dispatcher.h +++ b/include/livekit/subscription_thread_dispatcher.h @@ -29,6 +29,7 @@ #include "livekit/audio_stream.h" #include "livekit/video_stream.h" +#include "livekit/visibility.h" namespace livekit { @@ -83,7 +84,7 @@ using DataFrameCallbackId = std::uint64_t; * kinds can be added later without pushing more thread state back into * \ref Room. */ -class SubscriptionThreadDispatcher { +class LIVEKIT_API SubscriptionThreadDispatcher { public: /// Constructs an empty dispatcher with no registered callbacks or readers. SubscriptionThreadDispatcher(); diff --git a/include/livekit/tracing.h b/include/livekit/tracing.h index 1477bac6..a630ffe6 100644 --- a/include/livekit/tracing.h +++ b/include/livekit/tracing.h @@ -19,6 +19,8 @@ #include #include +#include "livekit/visibility.h" + namespace livekit { /** @@ -35,7 +37,7 @@ namespace livekit { * categories. * @return true if tracing was started, false if already running or file error */ -bool startTracing(const std::string& trace_file_path, const std::vector& categories = {}); +LIVEKIT_API bool startTracing(const std::string& trace_file_path, const std::vector& categories = {}); /** * Stop tracing and flush remaining events to file. @@ -43,13 +45,13 @@ bool startTracing(const std::string& trace_file_path, const std::vector #include #include @@ -25,6 +26,7 @@ #include "livekit/ffi_handle.h" #include "livekit/stats.h" +#include "livekit/visibility.h" namespace livekit { @@ -69,7 +71,7 @@ struct ParticipantTrackPermission { // ============================================================ // Base Track // ============================================================ -class Track { +class LIVEKIT_API Track { public: virtual ~Track() = default; diff --git a/include/livekit/track_publication.h b/include/livekit/track_publication.h index abed3a27..7a8d9ead 100644 --- a/include/livekit/track_publication.h +++ b/include/livekit/track_publication.h @@ -24,6 +24,7 @@ #include "livekit/e2ee.h" #include "livekit/ffi_handle.h" #include "livekit/track.h" +#include "livekit/visibility.h" namespace livekit { @@ -37,7 +38,7 @@ class RemoteTrack; * Wraps the immutable publication info plus an FFI handle, and * holds a weak reference to the associated Track (if any). */ -class TrackPublication { +class LIVEKIT_API TrackPublication { public: virtual ~TrackPublication() = default; diff --git a/include/livekit/video_frame.h b/include/livekit/video_frame.h index ecdae9a3..4520aeed 100644 --- a/include/livekit/video_frame.h +++ b/include/livekit/video_frame.h @@ -21,6 +21,8 @@ #include #include +#include "livekit/visibility.h" + namespace livekit { // Mirror of WebRTC video buffer type @@ -44,7 +46,7 @@ class OwnedVideoBuffer; * - The SDK can expose the backing memory to Rust via data_ptr + layout for * the duration of a blocking FFI call (similar to AudioFrame). */ -class VideoFrame { +class LIVEKIT_API VideoFrame { public: VideoFrame(); VideoFrame(int width, int height, VideoBufferType type, std::vector data); diff --git a/include/livekit/video_source.h b/include/livekit/video_source.h index e0d8544f..fa2f4378 100644 --- a/include/livekit/video_source.h +++ b/include/livekit/video_source.h @@ -20,6 +20,7 @@ #include #include "livekit/ffi_handle.h" +#include "livekit/visibility.h" namespace livekit { @@ -62,7 +63,7 @@ struct VideoCaptureOptions { * Represents a real-time video source that can accept frames from the * application and feed them into the LiveKit core. */ -class VideoSource { +class LIVEKIT_API VideoSource { public: /** * Create a new native video source with a fixed resolution. diff --git a/include/livekit/video_stream.h b/include/livekit/video_stream.h index 2ac9aca6..82874ad7 100644 --- a/include/livekit/video_stream.h +++ b/include/livekit/video_stream.h @@ -24,11 +24,12 @@ #include #include -#include "ffi_handle.h" -#include "participant.h" -#include "track.h" -#include "video_frame.h" -#include "video_source.h" +#include "livekit/ffi_handle.h" +#include "livekit/participant.h" +#include "livekit/track.h" +#include "livekit/video_frame.h" +#include "livekit/video_source.h" +#include "livekit/visibility.h" namespace livekit { @@ -63,7 +64,7 @@ class FfiEvent; // // stream->close(); // optional, called automatically in destructor // -class VideoStream { +class LIVEKIT_API VideoStream { public: struct Options { // Maximum number of VideoFrameEvent items buffered in the internal queue. diff --git a/include/livekit/visibility.h b/include/livekit/visibility.h new file mode 100644 index 00000000..c7921fb1 --- /dev/null +++ b/include/livekit/visibility.h @@ -0,0 +1,63 @@ +/* + * Copyright 2026 LiveKit + * + * 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. + */ + +#pragma once + +// LIVEKIT_API marks a symbol as part of the public ABI of liblivekit. +// +// On Unix, the SDK is built with -fvisibility=hidden / -fvisibility-inlines-hidden, +// so every symbol defaults to hidden. LIVEKIT_API re-exposes the symbol with +// default visibility. Consumers see no annotation (just a normal declaration). +// +// On Windows, the SDK is built without WINDOWS_EXPORT_ALL_SYMBOLS, so symbols +// must be explicitly tagged with __declspec(dllexport) when building the SDK +// and __declspec(dllimport) when consuming it. LIVEKIT_BUILDING_SDK is defined +// only when compiling the SDK itself (set in CMakeLists.txt). + +#if defined(_WIN32) +#if defined(LIVEKIT_BUILDING_SDK) +#define LIVEKIT_API __declspec(dllexport) +#else +#define LIVEKIT_API __declspec(dllimport) +#endif +#else +#if defined(LIVEKIT_BUILDING_SDK) +#define LIVEKIT_API __attribute__((visibility("default"))) +#else +#define LIVEKIT_API +#endif +#endif + +// LIVEKIT_INTERNAL_API marks a symbol that is NOT part of the public ABI but +// must remain visible so that the in-tree test binaries (which link against +// the same shared library) can resolve it. +// +// External consumers must not rely on LIVEKIT_INTERNAL_API symbols; they may +// change or disappear without notice. +// +// On Windows, internal symbols are exported the same way as public ones +// because tests link via the standard import library; on Unix, hidden +// visibility is overridden for these specific symbols only. + +#if defined(_WIN32) +#define LIVEKIT_INTERNAL_API LIVEKIT_API +#else +#if defined(LIVEKIT_BUILDING_SDK) +#define LIVEKIT_INTERNAL_API __attribute__((visibility("default"))) +#else +#define LIVEKIT_INTERNAL_API +#endif +#endif diff --git a/src/ffi_client.h b/src/ffi_client.h index 2879f2d2..97191586 100644 --- a/src/ffi_client.h +++ b/src/ffi_client.h @@ -32,6 +32,7 @@ #include "livekit/data_track_error.h" #include "livekit/result.h" #include "livekit/stats.h" +#include "livekit/visibility.h" #include "lk_log.h" #include "room.pb.h" @@ -61,8 +62,8 @@ extern "C" void livekit_ffi_dispose(); extern "C" void LivekitFfiCallback(const uint8_t* buf, size_t len); // The FfiClient is used to communicate with the FFI interface of the Rust SDK -// We use the generated protocol messages to facilitate the communication -class FfiClient { +// We use the generated protocol messages to facilitate the communication. +class LIVEKIT_INTERNAL_API FfiClient { public: using ListenerId = int; using Listener = std::function; diff --git a/src/lk_log.h b/src/lk_log.h index a42a441f..b5995744 100644 --- a/src/lk_log.h +++ b/src/lk_log.h @@ -21,15 +21,17 @@ #include +#include "livekit/visibility.h" + namespace livekit::detail { /// Returns the shared "livekit" logger instance. /// The logger is created lazily on first access and lives until /// shutdownLogger() is called. Safe to call before initialize(). -std::shared_ptr getLogger(); +LIVEKIT_INTERNAL_API std::shared_ptr getLogger(); /// Tears down the spdlog logger. Called by livekit::shutdown(). -void shutdownLogger(); +LIVEKIT_INTERNAL_API void shutdownLogger(); } // namespace livekit::detail diff --git a/src/logging.cpp b/src/logging.cpp index 59c51f0e..2e66e3a5 100644 --- a/src/logging.cpp +++ b/src/logging.cpp @@ -22,6 +22,8 @@ #include +#include "lk_log.h" + namespace livekit { namespace { diff --git a/src/room_proto_converter.h b/src/room_proto_converter.h index 8df7c795..8f502f31 100644 --- a/src/room_proto_converter.h +++ b/src/room_proto_converter.h @@ -19,6 +19,7 @@ #include #include "livekit/room_event_types.h" +#include "livekit/visibility.h" #include "room.pb.h" namespace livekit { @@ -30,62 +31,64 @@ struct TextStreamInfo; // --------- basic helper conversions --------- -ConnectionQuality toConnectionQuality(proto::ConnectionQuality in); -ConnectionState toConnectionState(proto::ConnectionState in); -DataPacketKind toDataPacketKind(proto::DataPacketKind in); -DisconnectReason toDisconnectReason(proto::DisconnectReason in); +LIVEKIT_INTERNAL_API ConnectionQuality toConnectionQuality(proto::ConnectionQuality in); +LIVEKIT_INTERNAL_API ConnectionState toConnectionState(proto::ConnectionState in); +LIVEKIT_INTERNAL_API DataPacketKind toDataPacketKind(proto::DataPacketKind in); +LIVEKIT_INTERNAL_API DisconnectReason toDisconnectReason(proto::DisconnectReason in); -UserPacketData fromProto(const proto::UserPacket& in); -SipDtmfData fromProto(const proto::SipDTMF& in); -RoomInfoData fromProto(const proto::RoomInfo& in); +LIVEKIT_INTERNAL_API UserPacketData fromProto(const proto::UserPacket& in); +LIVEKIT_INTERNAL_API SipDtmfData fromProto(const proto::SipDTMF& in); +LIVEKIT_INTERNAL_API RoomInfoData fromProto(const proto::RoomInfo& in); -DataStreamHeaderData fromProto(const proto::DataStream_Header& in); -DataStreamChunkData fromProto(const proto::DataStream_Chunk& in); -DataStreamTrailerData fromProto(const proto::DataStream_Trailer& in); +LIVEKIT_INTERNAL_API DataStreamHeaderData fromProto(const proto::DataStream_Header& in); +LIVEKIT_INTERNAL_API DataStreamChunkData fromProto(const proto::DataStream_Chunk& in); +LIVEKIT_INTERNAL_API DataStreamTrailerData fromProto(const proto::DataStream_Trailer& in); // --------- event conversions (RoomEvent.oneof message) --------- -RoomSidChangedEvent fromProto(const proto::RoomSidChanged& in); +LIVEKIT_INTERNAL_API RoomSidChangedEvent fromProto(const proto::RoomSidChanged& in); -ConnectionStateChangedEvent fromProto(const proto::ConnectionStateChanged& in); -DisconnectedEvent fromProto(const proto::Disconnected& in); -ReconnectingEvent fromProto(const proto::Reconnecting& in); -ReconnectedEvent fromProto(const proto::Reconnected& in); -RoomEosEvent fromProto(const proto::RoomEOS& in); +LIVEKIT_INTERNAL_API ConnectionStateChangedEvent fromProto(const proto::ConnectionStateChanged& in); +LIVEKIT_INTERNAL_API DisconnectedEvent fromProto(const proto::Disconnected& in); +LIVEKIT_INTERNAL_API ReconnectingEvent fromProto(const proto::Reconnecting& in); +LIVEKIT_INTERNAL_API ReconnectedEvent fromProto(const proto::Reconnected& in); +LIVEKIT_INTERNAL_API RoomEosEvent fromProto(const proto::RoomEOS& in); -DataStreamHeaderReceivedEvent fromProto(const proto::DataStreamHeaderReceived& in); -DataStreamChunkReceivedEvent fromProto(const proto::DataStreamChunkReceived& in); -DataStreamTrailerReceivedEvent fromProto(const proto::DataStreamTrailerReceived& in); +LIVEKIT_INTERNAL_API DataStreamHeaderReceivedEvent fromProto(const proto::DataStreamHeaderReceived& in); +LIVEKIT_INTERNAL_API DataStreamChunkReceivedEvent fromProto(const proto::DataStreamChunkReceived& in); +LIVEKIT_INTERNAL_API DataStreamTrailerReceivedEvent fromProto(const proto::DataStreamTrailerReceived& in); -DataChannelBufferedAmountLowThresholdChangedEvent fromProto( - const proto::DataChannelBufferedAmountLowThresholdChanged& in); +LIVEKIT_INTERNAL_API DataChannelBufferedAmountLowThresholdChangedEvent +fromProto(const proto::DataChannelBufferedAmountLowThresholdChanged& in); -ByteStreamOpenedEvent fromProto(const proto::ByteStreamOpened& in); -TextStreamOpenedEvent fromProto(const proto::TextStreamOpened& in); +LIVEKIT_INTERNAL_API ByteStreamOpenedEvent fromProto(const proto::ByteStreamOpened& in); +LIVEKIT_INTERNAL_API TextStreamOpenedEvent fromProto(const proto::TextStreamOpened& in); -RoomUpdatedEvent roomUpdatedFromProto(const proto::RoomInfo& in); // room_updated -RoomMovedEvent roomMovedFromProto(const proto::RoomInfo& in); // moved +LIVEKIT_INTERNAL_API RoomUpdatedEvent roomUpdatedFromProto(const proto::RoomInfo& in); // room_updated +LIVEKIT_INTERNAL_API RoomMovedEvent roomMovedFromProto(const proto::RoomInfo& in); // moved // --------- room options conversions --------- -proto::AudioEncoding toProto(const AudioEncodingOptions& in); -AudioEncodingOptions fromProto(const proto::AudioEncoding& in); +LIVEKIT_INTERNAL_API proto::AudioEncoding toProto(const AudioEncodingOptions& in); +LIVEKIT_INTERNAL_API AudioEncodingOptions fromProto(const proto::AudioEncoding& in); -proto::VideoEncoding toProto(const VideoEncodingOptions& in); -VideoEncodingOptions fromProto(const proto::VideoEncoding& in); +LIVEKIT_INTERNAL_API proto::VideoEncoding toProto(const VideoEncodingOptions& in); +LIVEKIT_INTERNAL_API VideoEncodingOptions fromProto(const proto::VideoEncoding& in); -proto::TrackPublishOptions toProto(const TrackPublishOptions& in); -TrackPublishOptions fromProto(const proto::TrackPublishOptions& in); +LIVEKIT_INTERNAL_API proto::TrackPublishOptions toProto(const TrackPublishOptions& in); +LIVEKIT_INTERNAL_API TrackPublishOptions fromProto(const proto::TrackPublishOptions& in); // --------- room Data Packet conversions --------- -UserDataPacketEvent userDataPacketFromProto(const proto::DataPacketReceived& in, RemoteParticipant* participant); +LIVEKIT_INTERNAL_API UserDataPacketEvent userDataPacketFromProto(const proto::DataPacketReceived& in, + RemoteParticipant* participant); -SipDtmfReceivedEvent sipDtmfFromProto(const proto::DataPacketReceived& in, RemoteParticipant* participant); +LIVEKIT_INTERNAL_API SipDtmfReceivedEvent sipDtmfFromProto(const proto::DataPacketReceived& in, + RemoteParticipant* participant); // --------- room Data Stream conversions --------- -std::map toAttrMap(const proto::DataStream::Trailer& trailer); -ByteStreamInfo makeByteInfo(const proto::DataStream::Header& header); -TextStreamInfo makeTextInfo(const proto::DataStream::Header& header); +LIVEKIT_INTERNAL_API std::map toAttrMap(const proto::DataStream::Trailer& trailer); +LIVEKIT_INTERNAL_API ByteStreamInfo makeByteInfo(const proto::DataStream::Header& header); +LIVEKIT_INTERNAL_API TextStreamInfo makeTextInfo(const proto::DataStream::Header& header); } // namespace livekit diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt index 9b52591d..2bd0c09e 100644 --- a/src/tests/CMakeLists.txt +++ b/src/tests/CMakeLists.txt @@ -51,10 +51,11 @@ if(UNIT_TEST_SOURCES) ${BENCHMARK_UTILS_SOURCES} ) - # On Windows, protobuf default-instance symbols (constinit globals) are not - # auto-exported from livekit.dll by WINDOWS_EXPORT_ALL_SYMBOLS. Link the - # proto object library directly so the test binary has its own copy. - if(WIN32 AND TARGET livekit_proto) + # liblivekit no longer re-exports protobuf/absl symbols (hidden visibility + + # --exclude-libs,ALL). Tests that touch livekit::proto::* types or absl + # helpers must link the proto object library and the libprotobuf static + # archive directly. + if(TARGET livekit_proto) target_sources(livekit_unit_tests PRIVATE $) endif() @@ -149,10 +150,11 @@ if(INTEGRATION_TEST_SOURCES) ${BENCHMARK_UTILS_SOURCES} ) - # On Windows, protobuf default-instance symbols (constinit globals) are not - # auto-exported from livekit.dll by WINDOWS_EXPORT_ALL_SYMBOLS. Link the - # proto object library directly so the test binary has its own copy. - if(WIN32 AND TARGET livekit_proto) + # liblivekit no longer re-exports protobuf/absl symbols (hidden visibility + + # --exclude-libs,ALL). Tests that touch livekit::proto::* types or absl + # helpers must link the proto object library and the libprotobuf static + # archive directly. + if(TARGET livekit_proto) target_sources(livekit_integration_tests PRIVATE $) endif() @@ -160,7 +162,7 @@ if(INTEGRATION_TEST_SOURCES) PRIVATE livekit spdlog::spdlog - $<$:${LIVEKIT_PROTOBUF_TARGET}> + ${LIVEKIT_PROTOBUF_TARGET} GTest::gtest_main ) diff --git a/src/trace/event_tracer.h b/src/trace/event_tracer.h index 2bb93e82..d99bc1cc 100644 --- a/src/trace/event_tracer.h +++ b/src/trace/event_tracer.h @@ -39,16 +39,7 @@ #ifndef LIVEKIT_TRACE_EVENT_TRACER_H_ #define LIVEKIT_TRACE_EVENT_TRACER_H_ -// Platform-specific DLL export macro -#if defined(_WIN32) -#if defined(LIVEKIT_BUILDING_SDK) -#define LIVEKIT_EXPORT __declspec(dllexport) -#else -#define LIVEKIT_EXPORT __declspec(dllimport) -#endif -#else -#define LIVEKIT_EXPORT __attribute__((visibility("default"))) -#endif +#include "livekit/visibility.h" namespace livekit { namespace trace { @@ -63,12 +54,11 @@ typedef void (*AddTraceEventPtr)(char phase, const unsigned char* category_enabl // // This method must be called before any tracing begins. Functions // provided should be thread-safe. -LIVEKIT_EXPORT void SetupEventTracer(GetCategoryEnabledPtr get_category_enabled_ptr, - AddTraceEventPtr add_trace_event_ptr); +LIVEKIT_API void SetupEventTracer(GetCategoryEnabledPtr get_category_enabled_ptr, AddTraceEventPtr add_trace_event_ptr); // This class defines interface for the event tracing system to call -// internally. Do not call these methods directly. -class EventTracer { +// internally. +class LIVEKIT_INTERNAL_API EventTracer { public: static const unsigned char* GetCategoryEnabled(const char* name); diff --git a/src/video_utils.h b/src/video_utils.h index c2c9bcfb..a6ddd3ec 100644 --- a/src/video_utils.h +++ b/src/video_utils.h @@ -18,17 +18,18 @@ #include "livekit/video_frame.h" #include "livekit/video_source.h" +#include "livekit/visibility.h" #include "video_frame.pb.h" namespace livekit { -// Video FFI Utils -proto::VideoBufferInfo toProto(const VideoFrame& frame); -VideoFrame fromOwnedProto(const proto::OwnedVideoBuffer& owned); -VideoFrame convertViaFfi(const VideoFrame& frame, VideoBufferType dst, bool flip_y); -proto::VideoBufferType toProto(const VideoBufferType t); -VideoBufferType fromProto(const proto::VideoBufferType t); -std::optional toProto(const std::optional& metadata); -std::optional fromProto(const proto::FrameMetadata& metadata); +// Video FFI Utils. +LIVEKIT_INTERNAL_API proto::VideoBufferInfo toProto(const VideoFrame& frame); +LIVEKIT_INTERNAL_API VideoFrame fromOwnedProto(const proto::OwnedVideoBuffer& owned); +LIVEKIT_INTERNAL_API VideoFrame convertViaFfi(const VideoFrame& frame, VideoBufferType dst, bool flip_y); +LIVEKIT_INTERNAL_API proto::VideoBufferType toProto(const VideoBufferType t); +LIVEKIT_INTERNAL_API VideoBufferType fromProto(const proto::VideoBufferType t); +LIVEKIT_INTERNAL_API std::optional toProto(const std::optional& metadata); +LIVEKIT_INTERNAL_API std::optional fromProto(const proto::FrameMetadata& metadata); } // namespace livekit