{"id":50996449,"url":"https://github.com/livekit/portal","last_synced_at":"2026-06-20T10:01:34.236Z","repository":{"id":354440130,"uuid":"1212864461","full_name":"livekit/portal","owner":"livekit","description":"A Simple Transport Layer For Teleoperation And Inference","archived":false,"fork":false,"pushed_at":"2026-06-09T15:45:19.000Z","size":8090,"stargazers_count":9,"open_issues_count":2,"forks_count":5,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-09T17:25:58.612Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/livekit.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":".github/CODEOWNERS","security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-16T20:07:58.000Z","updated_at":"2026-06-09T15:55:42.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/livekit/portal","commit_stats":null,"previous_names":["livekit/livekit-portal","livekit/portal"],"tags_count":9,"template":false,"template_full_name":null,"purl":"pkg:github/livekit/portal","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/livekit%2Fportal","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/livekit%2Fportal/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/livekit%2Fportal/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/livekit%2Fportal/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/livekit","download_url":"https://codeload.github.com/livekit/portal/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/livekit%2Fportal/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34565244,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-20T02:00:06.407Z","response_time":98,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-06-20T10:01:33.536Z","updated_at":"2026-06-20T10:01:34.224Z","avatar_url":"https://github.com/livekit.png","language":"Rust","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cpicture\u003e\n  \u003csource media=\"(prefers-color-scheme: dark)\" srcset=\"/.github/banner_dark.png\"\u003e\n  \u003csource media=\"(prefers-color-scheme: light)\" srcset=\"/.github/banner_light.png\"\u003e\n  \u003cimg style=\"width:100%;\" alt=\"The LiveKit icon, the name of the repository and some sample code in the background.\" src=\"https://raw.githubusercontent.com/livekit/portal/main/.github/banner_light.png\"\u003e\n\u003c/picture\u003e\n\n\u003ch1 align=\"center\"\u003eLiveKit Portal\u003c/h1\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003ca href=\"https://github.com/livekit/portal/actions/workflows/tests.yml\"\u003e\u003cimg src=\"https://github.com/livekit/portal/actions/workflows/tests.yml/badge.svg?branch=main\" alt=\"tests\"\u003e\u003c/a\u003e\n  \u003ca href=\"https://pypi.org/project/livekit-portal/\"\u003e\u003cimg src=\"https://img.shields.io/pypi/v/livekit-portal\" alt=\"PyPI\"\u003e\u003c/a\u003e\n  \u003ca href=\"LICENSE\"\u003e\u003cimg src=\"https://img.shields.io/badge/License-Apache_2.0-blue.svg\" alt=\"License\"\u003e\u003c/a\u003e\n  \u003ca href=\"https://www.python.org/downloads/\"\u003e\u003cimg src=\"https://img.shields.io/badge/python-3.10%2B-blue\" alt=\"Python 3.10+\"\u003e\u003c/a\u003e\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\".github/assets/portal-demo.gif\" alt=\"Portal demo: synced camera and joint state between a remote robot and a local operator\" width=\"720\"\u003e\n\u003c/p\u003e\n\n\u003c!--BEGIN_DESCRIPTION--\u003e\n\u003cp align=\"center\"\u003e\u003cb\u003eTeleoperate, run policies, and record demonstrations against the same robot, from anywhere on the internet, with multiple operators in the room at once.\u003c/b\u003e Portal carries cameras, joint state, and actions over LiveKit's room model. A policy and a human teleoperator can join the same session, hand off control mid-session with one call, and stream every executed action to a recorder for HITL training data. Synchronized \u003ccode\u003e(frames, state, timestamp)\u003c/code\u003e observations on the control side. Works with any robotics stack. Optional \u003ca href=\"https://github.com/huggingface/lerobot\"\u003eLeRobot\u003c/a\u003e plugin for a one-line drop-in.\u003c/p\u003e\n\u003c!--END_DESCRIPTION--\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003ca href=\"#quickstart\"\u003eQuickstart\u003c/a\u003e ·\n  \u003ca href=\"#examples\"\u003eExamples\u003c/a\u003e ·\n  \u003ca href=\"docs/03-portal-api.md\"\u003ePortal API\u003c/a\u003e ·\n  \u003ca href=\"docs/02-concepts.md\"\u003eConcepts\u003c/a\u003e ·\n  \u003ca href=\"docs/09-synchronization.md\"\u003eDeep dive\u003c/a\u003e\n\u003c/p\u003e\n\n---\n\n## Features\n\n**Multi-operator sessions.** A robot, policies, humans, recorders, and supervisors can all join the same session at once. The robot listens to whichever operator currently holds control. Other operators stream silently and are dropped at the gate. Handoff is `await op.set_active_operator(\"human-binh\")` from any participant. Built on LiveKit participant attributes plus one RPC method.\n\n- **Live human-in-the-loop.** Policy drives, human takes over to demonstrate corrections, policy resumes.\n- **HITL data recording.** A passive operator joins with `set_action_subscription(True)` and receives every executed action labeled with `action.sender` plus the matching observation. Fits in a 50-line script.\n- **Shadow evaluation.** Run a candidate policy alongside the active one. Both stream actions; only the active one is honored. The shadow records its outputs for offline comparison.\n- **Supervisor arbitration.** A participant that never sends actions can still call `set_active_operator(...)` to route control. Useful for human overseers, scheduling pipelines, or A/B routing.\n\n**Remote robot, same code.** Your robot loop keeps its shape. Portal moves the hardware to another machine. Your policy or teleop code still sees a local-looking robot object.\n\n**Synced observations out of the box.** Cameras and joint state arrive fused into `Observation(frames, state, timestamp_us)`. That is the shape robotics policies already consume. No matching logic on your side.\n\n**Built for VLA inference.** First-class **action chunks** ship a `(horizon, n_fields)` tensor in one packet via byte streams (no 15 KB cap). Tag every action with `in_reply_to_ts_us` and `metrics.policy.e2e_us_p50/p95` derives true observation→action latency, not just ping. See [`examples/python/inference/`](examples/python/inference) for a runnable VLA-style loop.\n\n**Frame video for policies.** WebRTC video is lossy and resamples colorspace. For inference where pixels matter, pass a non-H264 codec to [`add_video`](docs/05-frame-video.md) (`RAW`, `PNG`, or `MJPEG`) and each frame ships independently over a reliable byte stream. Same `send_video_frame` / `on_video_frame` API, RGB on both ends. MJPEG q=90 sustains 30 fps at 720p.\n\n**Works with any stack.** Role-specific `Robot` and `Operator` classes in Python and a unified `Portal` core in Rust. An optional [lerobot](https://github.com/huggingface/lerobot) plugin for a one-line wrap around your existing `Robot` or `Teleoperator`.\n\n**Low-latency transport.** WebRTC video (SIMD RGB→I420). SCTP data channels with reliable or unreliable delivery per stream. Byte streams for arbitrary-size payloads. RPC for one-shots like `home` or `calibrate`. Rust core, Python bindings via UniFFI.\n\n---\n\n## Quickstart\n\n### Install\n\n```bash\npip install livekit-portal\n```\n\nOr with uv:\n\n```bash\nuv add livekit-portal\n```\n\nPrebuilt wheels cover CPython 3.12 on Linux x86_64 (glibc ≥ 2.35), Linux aarch64 (glibc ≥ 2.39), and macOS Apple Silicon. On any other platform or Python version (Windows, Intel macOS, Python 3.10/3.11), build from source instead. The library itself supports Python 3.10+.\n\n**lerobot plugin.** If your stack uses [lerobot](https://github.com/huggingface/lerobot), install the matching plugin instead:\n\n```bash\npip install lerobot-robot-livekit          # robot side\npip install lerobot-teleoperator-livekit   # operator side\n```\n\n\u003cdetails\u003e\n\u003csummary\u003eBuild from source\u003c/summary\u003e\n\nYou need a [Rust toolchain](https://rustup.rs/) (stable `cargo`) and [`uv`](https://docs.astral.sh/uv/).\n\n```bash\ngit clone https://github.com/livekit/portal.git\ncd portal\n\nbash scripts/build_ffi_python.sh release\ncd python \u0026\u0026 uv sync\n```\n\n`build_ffi_python.sh` compiles the `livekit-portal-ffi` cdylib and generates the UniFFI Python bindings. On a cold machine this takes a few minutes. Rerun it whenever the Rust code changes.\n\n\u003c/details\u003e\n\n### Code\n\nA complete remote-robot session in two files. The robot host publishes\nframes and state, executes actions, and exposes a `home` RPC. The control\nhost receives synced observations, runs a policy, and calls `home` before\nthe control loop starts.\n\n**`robot.py`** runs on the machine the robot is plugged into.\n\n```python\nimport asyncio, time\nfrom livekit.portal import DType, Robot, RobotConfig\n\nasync def main():\n    cfg = RobotConfig(\"session-1\")\n    cfg.add_video(\"front\")                       # add more tracks for multi-camera\n    cfg.add_state_typed([(\"j1\", DType.F32), (\"j2\", DType.F32), (\"j3\", DType.F32)])\n    cfg.add_action_typed([(\"j1\", DType.F32), (\"j2\", DType.F32), (\"j3\", DType.F32)])\n    cfg.set_fps(30)\n\n    robot_portal = Robot(cfg)\n\n    # One-shot commands. Either side can register. Either side can invoke.\n    def on_home(_):\n        robot.home()\n        return \"ok\"\n    robot_portal.register_rpc_method(\"home\", on_home)\n\n    # Actions arrive here from whichever operator currently holds control.\n    # Other operators in the room are silently dropped at the gate.\n    robot_portal.on_action(lambda a: robot.send_action(a.values))\n\n    await robot_portal.connect(url, token)\n\n    while running:\n        obs = robot.get_observation()\n        ts = int(time.time() * 1_000_000)\n        robot_portal.send_video_frame(\"front\", obs.image, 640, 480, timestamp_us=ts)\n        robot_portal.send_state(obs.state, timestamp_us=ts)\n        await asyncio.sleep(1 / 30)\n\nasyncio.run(main())\n```\n\n**`operator.py`** runs wherever your policy or teleop UI lives.\n\n```python\nimport asyncio\nfrom livekit.portal import DType, Operator, OperatorConfig\n\nasync def main():\n    cfg = OperatorConfig(\"session-1\")\n    cfg.add_video(\"front\")\n    cfg.add_state_typed([(\"j1\", DType.F32), (\"j2\", DType.F32), (\"j3\", DType.F32)])\n    cfg.add_action_typed([(\"j1\", DType.F32), (\"j2\", DType.F32), (\"j3\", DType.F32)])\n    cfg.set_fps(30)\n\n    op = Operator(cfg)\n\n    # Cameras, state, and a sender timestamp arrive fused as one tuple.\n    def on_observation(obs):\n        # obs.frames[\"front\"], obs.state, obs.timestamp_us\n        # Pass in_reply_to_ts_us so metrics.policy.e2e_us_* measures\n        # true observation→action latency on the robot side.\n        op.send_action(policy(obs), in_reply_to_ts_us=obs.timestamp_us)\n\n    op.on_observation(on_observation)\n    await op.connect(url, token)\n\n    # Robot starts with `active_operator=None` and drops every action.\n    # Claim control to be the one whose actions are accepted.\n    await op.set_active_operator(op.local_identity())\n\n    await op.perform_rpc(\"home\")                 # imperative commands, not a loop\n    print(op.metrics())                          # RTT, sync delta, jitter, drops\n\n    while running:\n        await asyncio.sleep(1)\n\nasyncio.run(main())\n```\n\nThat is the whole surface at work in one page. Synced observations, an\naction callback, a control-plane claim, an RPC for one-shots, and a live\nmetrics snapshot. The code above is a sketch. For a runnable version with\ntoken minting already wired up, see [`examples/python/basic/`](examples/python/basic)\nor the step-by-step [Quickstart doc](docs/01-quickstart.md).\n\n## Behind the project\n\nTeleoperation over WAN is a networking problem before it is a robotics\nproblem. Low-latency video and control data have to traverse NAT,\nasymmetric bandwidth, jitter, and packet loss. WebRTC was built for\nexactly this, and [LiveKit](https://livekit.io/) wraps it in a\nproduction-grade SFU with a clean SDK. Portal builds the robotics layer\non top.\n\nThat layer exists because robotics policies want one bundled\n`Observation` per tick: cameras, joint state, and a timestamp arriving\ntogether. LiveKit's transport primitives do not deliver data that way.\nVideo tracks and data streams each have their own pacing, codec path,\nand retransmission. On the receiver they surface as independent event\nstreams arriving out of phase.\n\nPortal closes that gap. Every outgoing frame and state packet carries the\nsender's monotonic clock (packet-trailer metadata for video, a `u64`\nprefix for data). On the control side, a per-session `SyncBuffer` matches\nthem by sender timestamp:\n\n```text\nfor each head state S:\n    for each registered video track k:\n        F = nearest pending frame in track k to S\n        if |S - F| \u003c search_range:                   track k matches\n        elif track k's newest frame is past S + R:   drop the state\n        else:                                        wait for a newer frame\n\nif every track matched:\n    emit Observation { frames, state, timestamp_us: S }\n```\n\nThe real implementation is amortized `O(N + M)` through two-pointer\ncursors and blocker-gated short-circuiting, with `O(1)` unmatchability\ndetection. Full walkthrough in\n[docs/09-synchronization.md](docs/09-synchronization.md). The\n[Concepts](docs/02-concepts.md) page covers roles and the observation model.\n[Tuning](docs/06-tuning.md) covers `fps`, `slack`, and `tolerance`.\n\n## Multi-operator and HITL\n\nA Portal session is a room. Robot, policies, humans, recorders, and\nsupervisors all join the same one. The robot listens to one operator at\na time, named by an attribute it publishes\n(`lk.portal.active_operator`). Other operators' actions are dropped at\nthe gate. Handoff is one method call from any participant.\n\n```python\n# Policy is driving. Human takes over to demonstrate a correction.\nawait human.set_active_operator(human.local_identity())\n# ... human teleops for a bit ...\n# Hand back to policy.\nawait human.set_active_operator(\"policy-v1\")\n```\n\nFour common patterns:\n\n| Pattern                 | Who's in the room                                   | What changes                                                                                                                                                                      |\n| ----------------------- | --------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| **Single operator**     | robot + 1 operator                                  | Operator self-claims at startup.                                                                                                                                                  |\n| **HITL teleop**         | robot + policy + human                              | Either side calls `set_active_operator(...)` to switch. The robot's stream of executed actions is continuous across the cutover.                                                  |\n| **HITL data recording** | robot + policy + human + recorder                   | Recorder joins as a passive observer with `set_action_subscription(True)`. Receives every executed action labeled with `action.sender`, paired with the synchronized observation. |\n| **Shadow eval**         | robot + active policy + candidate policy + recorder | Candidate streams its actions; the gate drops them. Recorder captures both streams for offline divergence scoring.                                                                |\n| **Supervisor**          | robot + N operators + supervisor UI                 | Supervisor never claims control. Calls `set_active_operator(...)` to route control to whichever operator should be active.                                                        |\n\nBacking primitives:\n\n1. **Participant attributes** for the active-operator pointer. Server-managed, broadcast on change, included in JoinResponse for late joiners.\n2. **One RPC method** (`portal.set_active_operator`) for cross-participant writes to the robot's attribute.\n3. **The SFU's data fanout.** Every operator already receives every other operator's action packets; Portal adds a one-line gate keyed on `active_operator`.\n\nRecipes:\n[recorder example](python/packages/livekit-portal/tests/integration/test_action_subscription.py),\n[handoff tests](python/packages/livekit-portal/tests/integration/test_multi_operator.py).\n\n## Examples\n\nRunning examples is the fastest way to a known-good setup. Both live under\n[`examples/python/`](examples/python).\n\n**[`examples/python/basic/`](examples/python/basic)**\n\nNo hardware required. Uses the Portal API directly. Synthetic video and\nstate on one terminal, subscriber on another. Proves your LiveKit\ncredentials and native build are healthy.\n\n```bash\ncd examples/python/basic\ncp .env.example .env            # fill in LIVEKIT_URL / API_KEY / API_SECRET\nuv sync\nuv run robot.py                 # terminal 1\nuv run teleoperator.py          # terminal 2\n```\n\nThe same directory ships `robot_yaml.py` / `teleoperator_yaml.py`, which\nload the wire contract from a shared [`portal.yaml`](examples/python/basic/portal.yaml)\ninstead of declaring it in code. See [Config from YAML](docs/04-config-file.md)\nfor the schema reference.\n\n**[`examples/python/inference/`](examples/python/inference)**\n\nVLA-style remote inference. The robot streams obs to a remote \"policy\"\nwhich emits a `(horizon, n_fields)` **action chunk** per inference step.\nThe robot unrolls the chunk locally between rounds. Demonstrates the two\ninference-shaped features: `add_action_chunk` and `in_reply_to_ts_us`.\nReports live `metrics.policy.e2e_us_p50/p95`, the actual\nobservation→action latency, not network ping.\n\n```bash\ncd examples/python/inference\ncp .env.example .env\nuv sync\nuv run robot.py                 # terminal 1\nuv run policy.py                # terminal 2\n```\n\n**[`examples/python/so101/`](examples/python/so101)**\n\nReal hardware. Uses the lerobot plugin. A physical **SO-101 follower** is\ndriven by a remote **SO-101 leader**. Camera and joint state render in\n[rerun](https://rerun.io). Full calibration and wiring walkthrough in its\n[README](examples/python/so101/README.md).\n\n## Using with lerobot\n\nIf your stack is already on [lerobot](https://github.com/huggingface/lerobot),\ntwo optional plugin packages wrap the Portal code above. You pass in your\nexisting `Robot` or `Teleoperator` and the remote arm shows up as a local\nlerobot device to any workflow (teleop, dataset recording, policy eval). See\n[lerobot integration](docs/10-lerobot.md) for the full reference.\n\n## Why LiveKit\n\nPortal sits on LiveKit rather than raw WebRTC or a custom transport.\nThe choice keeps the codebase focused on robotics instead of plumbing.\n\n| What LiveKit gives you        | Why it matters for Portal                                                                                                                                                                |\n| ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| **Rooms with N participants** | A robot, two operators, a recorder, and a supervisor are the same session as 1:1. No new signaling, no mesh, no per-pair connection setup.                                               |\n| **Participant attributes**    | Server-managed key-value state per participant, broadcast on change, included in JoinResponse for late joiners. The active-operator pointer is one attribute on the robot.               |\n| **Cross-participant RPC**     | `portal.set_active_operator` is one method registered on the robot. Any operator calls it with one line.                                                                                 |\n| **Production SFU**            | A late joiner gets the full state without warm-up. Bandwidth is fanned out by the server, not by the robot.                                                                              |\n| **Tokens with attributes**    | Initial values like `active_operator` can be seeded at token-mint time so the robot starts focused on a specific operator before anyone connects. JWT-based permissions per participant. |\n| **Transport primitives**      | RTP media with pacing and bandwidth adaptation. SCTP data channels, reliable or unreliable. Typed byte streams with chunking. Portal maps observations straight onto these.              |\n| **Cross-language SDKs**       | Rust, Python, Swift, Kotlin, JavaScript, Unity. A browser teleop UI speaks the same protocol as the robot host.                                                                          |\n| **Deploy anywhere**           | [LiveKit Cloud](https://livekit.io/cloud) for zero ops, or self-host the open-source server. TURN relays handle NAT traversal.                                                           |\n| **Recording and egress**      | Server-side session recording is one webhook away.                                                                                                                                       |\n\nRunning on a single machine or a LAN-only robot? You do not need any of\nthis. A direct socket is enough.\n\n## Documentation\n\nStart with the [documentation overview](docs/00-overview.md) for a guided\nreading order. The pages, in sequence:\n\n| Page | What's in it |\n|---|---|\n| [0. Overview](docs/00-overview.md) | Map of the docs and how to navigate them |\n| [1. Quickstart](docs/01-quickstart.md) | Install, tokens, first run with `Robot` and `Operator` |\n| [2. Concepts](docs/02-concepts.md) | Roles, the observation model, multi-controller, frame format |\n| [3. Portal API](docs/03-portal-api.md) | The primary surface. `Robot`, `Operator`, callbacks, send methods, multi-controller |\n| [4. Config from YAML](docs/04-config-file.md) | Build `RobotConfig` / `OperatorConfig` from a shareable YAML file |\n| [5. Frame video](docs/05-frame-video.md) | Per-frame RGB over byte streams (RAW / PNG / MJPEG) for pixel-exact policies |\n| [6. Tuning](docs/06-tuning.md) | `fps`, `slack`, `tolerance`, asymmetric rates, reliability |\n| [7. RPC](docs/07-rpc.md) | Imperative commands (`home`, `calibrate`, ...) on top of LiveKit RPC |\n| [8. E2EE](docs/08-e2ee.md) | Shared-key end-to-end encryption for media and data |\n| [9. Synchronization deep dive](docs/09-synchronization.md) | The full match algorithm, cursor bookkeeping, complexity |\n| [10. lerobot integration](docs/10-lerobot.md) | The optional convenience plugins |\n| [11. Logging](docs/11-logging.md) | `RUST_LOG`, and what the tagged warnings mean and how to fix them |\n\n## License\n\nApache-2.0. See [LICENSE](LICENSE) for details.\n\n\u003cbr/\u003e\u003ctable\u003e\n\n\u003cthead\u003e\u003ctr\u003e\u003cth colspan=\"2\"\u003eLiveKit Ecosystem\u003c/th\u003e\u003c/tr\u003e\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\u003ctd\u003eAgents SDKs\u003c/td\u003e\u003ctd\u003e\u003ca href=\"https://github.com/livekit/agents\"\u003ePython\u003c/a\u003e · \u003ca href=\"https://github.com/livekit/agents-js\"\u003eNode.js\u003c/a\u003e\u003c/td\u003e\u003c/tr\u003e\u003ctr\u003e\u003c/tr\u003e\n\u003ctr\u003e\u003ctd\u003eLiveKit SDKs\u003c/td\u003e\u003ctd\u003e\u003ca href=\"https://github.com/livekit/client-sdk-js\"\u003eBrowser\u003c/a\u003e · \u003ca href=\"https://github.com/livekit/client-sdk-swift\"\u003eSwift\u003c/a\u003e · \u003ca href=\"https://github.com/livekit/client-sdk-android\"\u003eAndroid\u003c/a\u003e · \u003ca href=\"https://github.com/livekit/client-sdk-flutter\"\u003eFlutter\u003c/a\u003e · \u003ca href=\"https://github.com/livekit/client-sdk-react-native\"\u003eReact Native\u003c/a\u003e · \u003ca href=\"https://github.com/livekit/rust-sdks\"\u003eRust\u003c/a\u003e · \u003ca href=\"https://github.com/livekit/node-sdks\"\u003eNode.js\u003c/a\u003e · \u003ca href=\"https://github.com/livekit/python-sdks\"\u003ePython\u003c/a\u003e · \u003ca href=\"https://github.com/livekit/client-sdk-unity\"\u003eUnity\u003c/a\u003e · \u003ca href=\"https://github.com/livekit/client-sdk-unity-web\"\u003eUnity (WebGL)\u003c/a\u003e · \u003ca href=\"https://github.com/livekit/client-sdk-esp32\"\u003eESP32\u003c/a\u003e · \u003ca href=\"https://github.com/livekit/client-sdk-cpp\"\u003eC++\u003c/a\u003e\u003c/td\u003e\u003c/tr\u003e\u003ctr\u003e\u003c/tr\u003e\n\u003ctr\u003e\u003ctd\u003eStarter Apps\u003c/td\u003e\u003ctd\u003e\u003ca href=\"https://github.com/livekit-examples/agent-starter-python\"\u003ePython Agent\u003c/a\u003e · \u003ca href=\"https://github.com/livekit-examples/agent-starter-node\"\u003eTypeScript Agent\u003c/a\u003e · \u003ca href=\"https://github.com/livekit-examples/agent-starter-react\"\u003eReact App\u003c/a\u003e · \u003ca href=\"https://github.com/livekit-examples/agent-starter-swift\"\u003eSwiftUI App\u003c/a\u003e · \u003ca href=\"https://github.com/livekit-examples/agent-starter-android\"\u003eAndroid App\u003c/a\u003e · \u003ca href=\"https://github.com/livekit-examples/agent-starter-flutter\"\u003eFlutter App\u003c/a\u003e · \u003ca href=\"https://github.com/livekit-examples/agent-starter-react-native\"\u003eReact Native App\u003c/a\u003e · \u003ca href=\"https://github.com/livekit-examples/agent-starter-embed\"\u003eWeb Embed\u003c/a\u003e\u003c/td\u003e\u003c/tr\u003e\u003ctr\u003e\u003c/tr\u003e\n\u003ctr\u003e\u003ctd\u003eUI Components\u003c/td\u003e\u003ctd\u003e\u003ca href=\"https://github.com/livekit/components-js\"\u003eReact\u003c/a\u003e · \u003ca href=\"https://github.com/livekit/components-android\"\u003eAndroid Compose\u003c/a\u003e · \u003ca href=\"https://github.com/livekit/components-swift\"\u003eSwiftUI\u003c/a\u003e · \u003ca href=\"https://github.com/livekit/components-flutter\"\u003eFlutter\u003c/a\u003e\u003c/td\u003e\u003c/tr\u003e\u003ctr\u003e\u003c/tr\u003e\n\u003ctr\u003e\u003ctd\u003eServer APIs\u003c/td\u003e\u003ctd\u003e\u003ca href=\"https://github.com/livekit/node-sdks\"\u003eNode.js\u003c/a\u003e · \u003ca href=\"https://github.com/livekit/server-sdk-go\"\u003eGolang\u003c/a\u003e · \u003ca href=\"https://github.com/livekit/server-sdk-ruby\"\u003eRuby\u003c/a\u003e · \u003ca href=\"https://github.com/livekit/server-sdk-kotlin\"\u003eJava/Kotlin\u003c/a\u003e · \u003ca href=\"https://github.com/livekit/python-sdks\"\u003ePython\u003c/a\u003e · \u003ca href=\"https://github.com/livekit/rust-sdks\"\u003eRust\u003c/a\u003e · \u003ca href=\"https://github.com/agence104/livekit-server-sdk-php\"\u003ePHP (community)\u003c/a\u003e · \u003ca href=\"https://github.com/pabloFuente/livekit-server-sdk-dotnet\"\u003e.NET (community)\u003c/a\u003e\u003c/td\u003e\u003c/tr\u003e\u003ctr\u003e\u003c/tr\u003e\n\u003ctr\u003e\u003ctd\u003eResources\u003c/td\u003e\u003ctd\u003e\u003ca href=\"https://docs.livekit.io\"\u003eDocs\u003c/a\u003e · \u003ca href=\"https://docs.livekit.io/mcp\"\u003eDocs MCP Server\u003c/a\u003e · \u003ca href=\"https://github.com/livekit/livekit-cli\"\u003eCLI\u003c/a\u003e · \u003ca href=\"https://cloud.livekit.io\"\u003eLiveKit Cloud\u003c/a\u003e\u003c/td\u003e\u003c/tr\u003e\u003ctr\u003e\u003c/tr\u003e\n\u003ctr\u003e\u003ctd\u003eLiveKit Server OSS\u003c/td\u003e\u003ctd\u003e\u003ca href=\"https://github.com/livekit/livekit\"\u003eLiveKit server\u003c/a\u003e · \u003ca href=\"https://github.com/livekit/egress\"\u003eEgress\u003c/a\u003e · \u003ca href=\"https://github.com/livekit/ingress\"\u003eIngress\u003c/a\u003e · \u003ca href=\"https://github.com/livekit/sip\"\u003eSIP\u003c/a\u003e\u003c/td\u003e\u003c/tr\u003e\u003ctr\u003e\u003c/tr\u003e\n\u003ctr\u003e\u003ctd\u003eCommunity\u003c/td\u003e\u003ctd\u003e\u003ca href=\"https://community.livekit.io\"\u003eDeveloper Community\u003c/a\u003e · \u003ca href=\"https://livekit.io/join-slack\"\u003eSlack\u003c/a\u003e · \u003ca href=\"https://x.com/livekit\"\u003eX\u003c/a\u003e · \u003ca href=\"https://www.youtube.com/@livekit_io\"\u003eYouTube\u003c/a\u003e\u003c/td\u003e\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flivekit%2Fportal","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Flivekit%2Fportal","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flivekit%2Fportal/lists"}