Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .github/workflows/builds.yml
Original file line number Diff line number Diff line change
Expand Up @@ -108,14 +108,15 @@ jobs:
libssl-dev \
libprotobuf-dev protobuf-compiler \
libabsl-dev \
libwayland-dev libdecor-0-dev
libwayland-dev libdecor-0-dev \
libspdlog-dev

- name: Install deps (macOS)
if: runner.os == 'macOS'
run: |
set -eux
brew update
brew install cmake ninja protobuf abseil
brew install cmake ninja protobuf abseil spdlog

# ---------- Rust toolchain ----------
- name: Install Rust (stable)
Expand Down
2 changes: 2 additions & 0 deletions bridge/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ add_library(livekit_bridge SHARED
src/bridge_video_track.cpp
src/bridge_room_delegate.cpp
src/bridge_room_delegate.h
src/rpc_controller.cpp
src/rpc_controller.h
)

if(WIN32)
Expand Down
23 changes: 21 additions & 2 deletions bridge/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,21 @@ bridge.setOnVideoFrameCallback("remote-peer", livekit::TrackSource::SOURCE_CAMER
// Called on a background reader thread
});

// 5. Cleanup is automatic (RAII), or explicit:
// 5. RPC (Remote Procedure Call)
bridge.registerRpcMethod("greet",
[](const livekit::RpcInvocationData& data) -> std::optional<std::string> {
return "Hello, " + data.caller_identity + "!";
});

std::string response = bridge.performRpc("remote-peer", "greet", "");

bridge.unregisterRpcMethod("greet");

// Controller side: send commands to the publisher
controller_bridge.requestRemoteTrackMute("robot-1", "mic"); // mute audio track "mic"
controller_bridge.requestRemoteTrackUnmute("robot-1", "mic"); // unmute it

// 7. Cleanup is automatic (RAII), or explicit:
mic.reset(); // unpublishes the audio track
cam.reset(); // unpublishes the video track
bridge.disconnect();
Expand Down Expand Up @@ -138,6 +152,11 @@ bridge.connect(url, token, options);
| `setOnVideoFrameCallback(identity, source, callback)` | Register a callback for video frames from a specific remote participant + track source. |
| `clearOnAudioFrameCallback(identity, source)` | Clear the audio callback for a specific remote participant + track source. Stops and joins the reader thread if active. |
| `clearOnVideoFrameCallback(identity, source)` | Clear the video callback for a specific remote participant + track source. Stops and joins the reader thread if active. |
| `performRpc(destination_identity, method, payload, response_timeout?)` | Blocking RPC call to a remote participant. Returns the response payload. Throws `livekit::RpcError` on failure. |
| `registerRpcMethod(method_name, handler)` | Register a handler for incoming RPC invocations. The handler returns an optional response payload or throws `livekit::RpcError`. |
| `unregisterRpcMethod(method_name)` | Unregister a previously registered RPC handler. |
| `requestRemoteTrackMute(identity, track_name)` | Ask a remote participant to mute a track by name. Throws `livekit::RpcError` on failure. |
| `requestRemoteTrackUnmute(identity, track_name)` | Ask a remote participant to unmute a track by name. Throws `livekit::RpcError` on failure. |

### `BridgeAudioTrack`

Expand Down Expand Up @@ -240,7 +259,7 @@ The bridge is designed for simplicity and currently only supports limited audio

- We dont support all events defined in the RoomDelegate interface.
- E2EE configuration
- RPC / data channels / data tracks
- data tracks
- Simulcast tuning
- Video format selection (RGBA is the default; no format option yet)
- Custom `RoomOptions` or `TrackPublishOptions`
Expand Down
96 changes: 96 additions & 0 deletions bridge/include/livekit_bridge/livekit_bridge.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,11 @@

#include "livekit_bridge/bridge_audio_track.h"
#include "livekit_bridge/bridge_video_track.h"
#include "livekit_bridge/rpc_constants.h"

#include "livekit/local_participant.h"
#include "livekit/room.h"
#include "livekit/rpc_error.h"

#include <cstdint>
#include <functional>
Expand All @@ -46,6 +49,7 @@ enum class TrackSource;
namespace livekit_bridge {

class BridgeRoomDelegate;
class RpcController;

namespace test {
class CallbackKeyTest;
Expand Down Expand Up @@ -264,6 +268,90 @@ class LiveKitBridge {
void clearOnVideoFrameCallback(const std::string &participant_identity,
livekit::TrackSource source);

// ---------------------------------------------------------------
// RPC (Remote Procedure Call)
// ---------------------------------------------------------------

/**
* Initiate a blocking RPC call to a remote participant.
*
* Sends a request to the participant identified by
* @p destination_identity and blocks until a response is received
* or the call times out.
*
* @param destination_identity Identity of the remote participant.
* @param method Name of the RPC method to invoke.
* @param payload Request payload string.
* @param response_timeout Optional timeout in seconds. If not set,
* the server default (15 s) is used.
* @return The response payload returned by the remote handler. nullptr if the
* RPC call fails, or the bridge is not connected.
*/
std::optional<std::string>
performRpc(const std::string &destination_identity, const std::string &method,
const std::string &payload,
const std::optional<double> &response_timeout = std::nullopt);

/**
* Register a handler for incoming RPC method invocations.
*
* When a remote participant calls the given @p method_name on this
* participant, the bridge invokes @p handler. The handler may return
* an optional response payload or throw a @c livekit::RpcError to
* signal failure to the caller.
*
* If a handler is already registered for @p method_name, it is
* silently replaced.
*
* @param method_name Name of the RPC method to handle.
* @param handler Callback invoked on each incoming invocation.
* @return true if the RPC method was registered successfully.
*/
bool registerRpcMethod(const std::string &method_name,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RpcMethod is pretty low level, I wonder if we should make a remote controller, and we will setup a remoteControlInternal method as the communication channel for the remote control

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

im not sure i understand the need to break this into another layer. My intention here was to have a single object (currently RPCManager) which does all the interfacing with the local participant required to register/make/receive RPC calls. I wanted to keep the function of registerRpcMethod here the same as it is in the local participant for clarity.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

who is going to call this registerRpcMethod() ? like developers ? or it will be called by your RpcController / RpcManager ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a developer in their application would call: livekit_bridge->registerRpcMethod("my-awesome-rpc", awesome_rpc_handle) which simply wraps around the RpcController

livekit::LocalParticipant::RpcHandler handler);

/**
* Unregister a previously registered RPC method handler.
*
* After this call, invocations for @p method_name result in an
* "unsupported method" error being returned to the remote caller.
* If no handler is registered for this name, the call is a no-op.
*
* @param method_name Name of the RPC method to unregister.
* @return true if the RPC method was unregistered successfully.
*/
bool unregisterRpcMethod(const std::string &method_name);

// ---------------------------------------------------------------
// Remote Track Control (via RPC)
// ---------------------------------------------------------------

/**
* Request a remote participant to mute a published track.
*
* The remote participant must be a LiveKitBridge instance (which
* automatically registers the built-in track-control RPC handler).
*
* @param destination_identity Identity of the remote participant.
* @param track_name Name of the track to mute.
* @return true if the track was muted successfully.
*/
bool requestRemoteTrackMute(const std::string &destination_identity,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These APIs are very concerning and can be very confusing.

What if the remote participant is not a livektiBridge (or SessionManager after the rename )?

How do you make sure these actions are backward compatitable ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These built in RPC calls only work from session manager to session manager, since the RpcController belongs to the session manager. Of course users can register their own remote track mute/unmute RPC methods!

const std::string &track_name);

/**
* Request a remote participant to unmute a published track.
*
* The remote participant must be a LiveKitBridge instance (which
* automatically registers the built-in track-control RPC handler).
*
* @param destination_identity Identity of the remote participant.
* @param track_name Name of the track to unmute.
* @return true if the track was unmuted successfully.
*/
bool requestRemoteTrackUnmute(const std::string &destination_identity,
const std::string &track_name);

private:
friend class BridgeRoomDelegate;
friend class test::CallbackKeyTest;
Expand Down Expand Up @@ -314,6 +402,13 @@ class LiveKitBridge {
const std::shared_ptr<livekit::Track> &track,
VideoFrameCallback cb);

/// Execute a track action (mute/unmute) by track name.
/// Used as the TrackActionFn callback for RpcController.
/// Throws livekit::RpcError if the track is not found.
/// @pre Caller does NOT hold mutex_ (acquires it internally).
void executeTrackAction(const rpc::track_control::Action &action,
const std::string &track_name);

mutable std::mutex mutex_;
bool connected_;
bool connecting_; // guards against concurrent connect() calls
Expand All @@ -323,6 +418,7 @@ class LiveKitBridge {

std::unique_ptr<livekit::Room> room_;
std::unique_ptr<BridgeRoomDelegate> delegate_;
std::unique_ptr<RpcController> rpc_controller_;

/// Registered callbacks (may be registered before tracks are subscribed).
std::unordered_map<CallbackKey, AudioFrameCallback, CallbackKeyHash>
Expand Down
63 changes: 63 additions & 0 deletions bridge/include/livekit_bridge/rpc_constants.h
Original file line number Diff line number Diff line change
@@ -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.
*/

/// @file rpc_constants.h
/// @brief Constants for built-in bridge RPC methods.

#pragma once

#include <string>

namespace livekit_bridge {
namespace rpc {

/// Built-in RPC method name used by remote track control.
/// Allows remote participants to mute or unmute tracks
/// published by this bridge. Must be called after connect().
/// Audio/video tracks support mute and unmute. Data tracks
/// only support mute and unmute.
namespace track_control {

enum class Action { kActionMute, kActionUnmute };

/// RPC method name registered by the bridge for remote track control.
constexpr const char *kMethod = "lk.bridge.track-control";

/// Payload action strings.
constexpr const char *kActionMute = "mute";
constexpr const char *kActionUnmute = "unmute";

/// Delimiter between action and track name in the payload (e.g. "mute:cam").
constexpr char kDelimiter = ':';

/// Response payload returned on success.
constexpr const char *kResponseOk = "ok";

/// Build a track-control RPC payload: "<action>:<track_name>".
inline std::string formatPayload(const char *action,
const std::string &track_name) {
std::string payload;
payload.reserve(std::char_traits<char>::length(action) + 1 +
track_name.size());
payload += action;
payload += kDelimiter;
payload += track_name;
return payload;
}

} // namespace track_control
} // namespace rpc
} // namespace livekit_bridge
Loading
Loading