Skip to content
Draft
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
19 changes: 19 additions & 0 deletions include/livekit/room.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,17 @@
#pragma once

#include <cstdint>
#include <future>
#include <memory>
#include <mutex>

#include "livekit/data_stream.h"
#include "livekit/e2ee.h"
#include "livekit/ffi_handle.h"
#include "livekit/result.h"
#include "livekit/room_event_types.h"
#include "livekit/session_stats_error.h"
#include "livekit/stats.h"
#include "livekit/subscription_thread_dispatcher.h"
#include "livekit/visibility.h"

Expand Down Expand Up @@ -187,6 +191,21 @@ class LIVEKIT_API Room {
/// Returns the current connection state of the room.
ConnectionState connectionState() const;

/// Retrieve aggregated WebRTC stats for this room session.
///
/// Behavior:
/// - If the room is not currently connected (no live FFI handle), resolves
/// immediately with a `GetSessionStatsErrorCode::NOT_CONNECTED` failure.
/// - Otherwise dispatches an async `get_session_stats` request to the Rust
/// FFI; the future resolves once the corresponding callback arrives.
/// - The future never throws — failures are surfaced as a typed
/// `GetSessionStatsError`. Inspect `Result::ok()` / `Result::error().code`
/// to branch on outcome.
///
/// @return Future resolving with publisher + subscriber stats on success,
/// or a typed error code + message on failure.
std::future<Result<SessionStats, GetSessionStatsError>> getSessionStats() const;

/* Register a handler for incoming text streams on a specific topic.
*
* When a remote participant opens a text stream with the given topic,
Expand Down
49 changes: 49 additions & 0 deletions include/livekit/session_stats_error.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* 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

#include <cstdint>
#include <string>

namespace livekit {

/// Categorical reason code for a failed `Room::getSessionStats()` call.
enum class GetSessionStatsErrorCode : std::uint32_t {
/// Catch-all: the FFI returned an error message that does not map to a more
/// specific code.
UNKNOWN = 0,
/// The `Room` has no live FFI handle (never connected or already
/// disconnected).
NOT_CONNECTED = 1,
/// The FFI responded with an unexpected response shape (e.g. a missing
/// `get_session_stats` field on the synchronous response).
PROTOCOL_ERROR = 2,
/// The FFI threw an internal error while servicing the request (e.g. the
/// underlying Rust engine reported a failure).
INTERNAL = 3,
};

/// Typed error returned by `Room::getSessionStats()`.
///
/// Surfaces the error reason as a `GetSessionStatsErrorCode` plus an
/// implementation-defined message for diagnostics/logging.
struct GetSessionStatsError {
GetSessionStatsErrorCode code{GetSessionStatsErrorCode::UNKNOWN};
std::string message;
};

} // namespace livekit
15 changes: 15 additions & 0 deletions include/livekit/stats.h
Original file line number Diff line number Diff line change
Expand Up @@ -501,6 +501,21 @@ struct RtcStats {
RtcStatsVariant stats;
};

/// Aggregated WebRTC stats for a connected room session.
///
/// Mirrors the FFI `GetSessionStatsCallback.Result` payload: stats are split
/// between the publisher peer connection (outbound media flowing from the
/// local participant to the SFU) and the subscriber peer connection (inbound
/// media flowing from the SFU back to the local participant). When the SDK is
/// operating in single-peer-connection mode the publisher list carries the
/// combined stats and the subscriber list is empty.
struct SessionStats {
/// Stats from the publisher peer connection (outbound media).
std::vector<RtcStats> publisher_stats;
/// Stats from the subscriber peer connection (inbound media).
std::vector<RtcStats> subscriber_stats;
};

// ----------------------
// fromProto declarations
// ----------------------
Expand Down
67 changes: 67 additions & 0 deletions src/ffi_client.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,73 @@ std::future<std::vector<RtcStats>> FfiClient::getTrackStatsAsync(uintptr_t track
return fut;
}

namespace {

std::future<Result<SessionStats, GetSessionStatsError>> readySessionStatsFailure(GetSessionStatsErrorCode code,
std::string message) {
std::promise<Result<SessionStats, GetSessionStatsError>> pr;
pr.set_value(Result<SessionStats, GetSessionStatsError>::failure(GetSessionStatsError{code, std::move(message)}));
return pr.get_future();
}

} // namespace

std::future<Result<SessionStats, GetSessionStatsError>> FfiClient::getSessionStatsAsync(uintptr_t room_handle) {
const AsyncId async_id = generateAsyncId();

auto fut = registerAsync<Result<SessionStats, GetSessionStatsError>>(
async_id,
// match
[async_id](const proto::FfiEvent& event) {
return event.has_get_session_stats() && event.get_session_stats().async_id() == async_id;
},
// handler
[](const proto::FfiEvent& event, std::promise<Result<SessionStats, GetSessionStatsError>>& pr) {
const auto& cb = event.get_session_stats();
if (cb.has_error()) {
pr.set_value(Result<SessionStats, GetSessionStatsError>::failure(
GetSessionStatsError{GetSessionStatsErrorCode::INTERNAL, cb.error()}));
return;
}
if (!cb.has_result()) {
pr.set_value(Result<SessionStats, GetSessionStatsError>::failure(GetSessionStatsError{
GetSessionStatsErrorCode::PROTOCOL_ERROR, "GetSessionStatsCallback missing result and error"}));
return;
}

const auto& result = cb.result();
SessionStats stats;
stats.publisher_stats.reserve(result.publisher_stats_size());
for (const auto& ps : result.publisher_stats()) {
stats.publisher_stats.push_back(fromProto(ps));
}
stats.subscriber_stats.reserve(result.subscriber_stats_size());
for (const auto& ps : result.subscriber_stats()) {
stats.subscriber_stats.push_back(fromProto(ps));
}
pr.set_value(Result<SessionStats, GetSessionStatsError>::success(std::move(stats)));
});

proto::FfiRequest req;
auto* get_session_stats_req = req.mutable_get_session_stats();
get_session_stats_req->set_room_handle(room_handle);
get_session_stats_req->set_request_async_id(async_id);

try {
const proto::FfiResponse resp = sendRequest(req);
if (!resp.has_get_session_stats()) {
cancelPendingByAsyncId(async_id);
return readySessionStatsFailure(GetSessionStatsErrorCode::PROTOCOL_ERROR,
"FfiResponse missing get_session_stats");
}
} catch (const std::exception& e) {
cancelPendingByAsyncId(async_id);
return readySessionStatsFailure(GetSessionStatsErrorCode::INTERNAL, e.what());
}

return fut;
}

// Participant APIs Implementation
std::future<proto::OwnedTrackPublication> FfiClient::publishTrackAsync(std::uint64_t local_participant_handle,
std::uint64_t track_handle,
Expand Down
4 changes: 4 additions & 0 deletions src/ffi_client.h
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
#include "data_track.pb.h"
#include "livekit/data_track_error.h"
#include "livekit/result.h"
#include "livekit/session_stats_error.h"
#include "livekit/stats.h"
#include "livekit/visibility.h"
#include "lk_log.h"
Expand Down Expand Up @@ -97,6 +98,9 @@ class LIVEKIT_INTERNAL_API FfiClient {
// Track APIs
std::future<std::vector<RtcStats>> getTrackStatsAsync(uintptr_t track_handle);

// Room APIs (stats)
std::future<Result<SessionStats, GetSessionStatsError>> getSessionStatsAsync(uintptr_t room_handle);

// Participant APIs
std::future<proto::OwnedTrackPublication> publishTrackAsync(std::uint64_t local_participant_handle,
std::uint64_t track_handle,
Expand Down
15 changes: 15 additions & 0 deletions src/room.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,21 @@ ConnectionState Room::connectionState() const {
return connection_state_;
}

std::future<Result<SessionStats, GetSessionStatsError>> Room::getSessionStats() const {
std::shared_ptr<FfiHandle> handle;
{
const std::scoped_lock<std::mutex> g(lock_);
handle = room_handle_;
}
if (!handle) {
std::promise<Result<SessionStats, GetSessionStatsError>> pr;
pr.set_value(Result<SessionStats, GetSessionStatsError>::failure(
GetSessionStatsError{GetSessionStatsErrorCode::NOT_CONNECTED, "Room is not connected"}));
return pr.get_future();
}
return FfiClient::instance().getSessionStatsAsync(handle->get());
}

E2EEManager* Room::e2eeManager() const {
const std::scoped_lock<std::mutex> g(lock_);
return e2ee_manager_.get();
Expand Down
Loading
Loading