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
4 changes: 4 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,10 @@ jobs:
env:
RUST_LOG: "metrics=debug"
LIVEKIT_CREATE_TOKEN_URL: ${{ steps.token_server.outputs.token-url }}
# Dev-server credentials so the admin-driven server-disconnect tests
# (which shell out to `lk`) run instead of skipping.
LIVEKIT_API_KEY: devkey
LIVEKIT_API_SECRET: secret
run: |
set -euo pipefail
source .token_helpers/set_data_track_test_tokens.bash
Expand Down
221 changes: 221 additions & 0 deletions docs/connection-lifecycle.md

Large diffs are not rendered by default.

21 changes: 21 additions & 0 deletions include/livekit/room.h
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,27 @@ class LIVEKIT_API Room {
/// room was already disconnected (no-op) or the graceful disconnect fails.
bool disconnect(DisconnectReason reason = DisconnectReason::ClientInitiated);

/// Inject a reconnection / chaos scenario for testing.
///
/// Drives the Rust core's `SimulateScenario` in-band over the live signalling
/// connection. This exists to exercise the server-initiated reconnect and
/// leave paths (which no other client API reaches) end-to-end without
/// server-admin calls. It is intended for tests and chaos tooling, not
/// production traffic.
///
/// Depending on the scenario, expect subsequent delegate callbacks such as
/// @ref RoomDelegate::onReconnecting / @ref RoomDelegate::onReconnected (the
/// reconnect scenarios) or @ref RoomDelegate::onDisconnected (ServerLeave).
///
/// @warning Safe to call from any thread, but **must not** be called from
/// inside a `RoomDelegate` callback — doing so will deadlock the event
/// listener, since this blocks on the FFI callback delivered on that thread.
///
/// @param scenario The scenario to inject.
/// @returns true if the FFI accepted the request; false if it failed.
/// @throws std::runtime_error if the room is not currently connected.
bool simulateScenario(SimulateScenario scenario);

// Accessors

/// Retrieve static metadata about the room.
Expand Down
30 changes: 30 additions & 0 deletions include/livekit/room_event_types.h
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,36 @@ enum class DisconnectReason {
MediaFailure
};

/// Reconnection / chaos scenarios that can be injected at runtime for testing.
///
/// These map 1:1 to the Rust core's `SimulateScenario` (proto
/// `SimulateScenarioKind`) and are driven in-band over the live signalling
/// connection via @ref Room::simulateScenario. They exist to exercise the
/// server-initiated reconnect and leave paths (which no client API otherwise
/// reaches) without needing server-admin calls. Intended for tests and chaos
/// tooling, not production traffic.
enum class SimulateScenario {
/// Close the signal channel locally; the engine attempts a resume.
SignalReconnect = 0,
/// Simulate local speaker activity (surfaces via onActiveSpeakersChanged).
Speaker,
/// Simulate a media node failure, triggering reconnection.
NodeFailure,
/// Ask the server to send a Leave, producing a server-initiated disconnect.
ServerLeave,
/// Simulate a server migration.
Migration,
/// Force ICE to use TCP, triggering a transport reconnect.
ForceTcp,
/// Force ICE to use TLS, triggering a transport reconnect.
ForceTls,
/// Ask the server to send Leave{Reconnect}, forcing a full reconnect
/// (new session; the SDK republishes existing local tracks).
FullReconnect,
/// Drop signalling mid-resume, forcing the resume->full-reconnect escalation.
DisconnectSignalOnResume
};

/// Application-level user data carried in a data packet.
struct UserPacketData {
/// Raw payload bytes.
Expand Down
38 changes: 38 additions & 0 deletions src/ffi_client.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ std::optional<FfiClient::AsyncId> ExtractAsyncId(const proto::FfiEvent& event) {
return event.connect().async_id();
case E::kDisconnect:
return event.disconnect().async_id();
case E::kSimulateScenario:
return event.simulate_scenario().async_id();
case E::kDispose:
return event.dispose().async_id();
case E::kPublishTrack:
Expand Down Expand Up @@ -540,6 +542,42 @@ std::future<void> FfiClient::disconnectAsync(uintptr_t room_handle, DisconnectRe
return fut;
}

std::future<void> FfiClient::simulateScenarioAsync(uintptr_t room_handle, SimulateScenario scenario) {
const AsyncId async_id = generateAsyncId();

auto fut = registerAsync<void>(
async_id,
[async_id](const proto::FfiEvent& event) {
return event.has_simulate_scenario() && event.simulate_scenario().async_id() == async_id;
},
[](const proto::FfiEvent& event, std::promise<void>& pr) {
const auto& cb = event.simulate_scenario();
if (cb.has_error() && !cb.error().empty()) {
pr.set_exception(std::make_exception_ptr(std::runtime_error(cb.error())));
return;
}
pr.set_value();
});

proto::FfiRequest req;
auto* simulate = req.mutable_simulate_scenario();
simulate->set_room_handle(room_handle);
simulate->set_request_async_id(async_id);
simulate->set_scenario(toProto(scenario));

try {
const proto::FfiResponse resp = sendRequest(req);
if (!resp.has_simulate_scenario()) {
logAndThrow("FfiResponse missing simulate_scenario");
}
} catch (...) {
cancelPendingByAsyncId(async_id);
throw;
}

return fut;
}

// Track APIs Implementation
std::future<std::vector<RtcStats>> FfiClient::getTrackStatsAsync(uintptr_t track_handle) {
// Generate client-side async_id first
Expand Down
3 changes: 3 additions & 0 deletions src/ffi_client.h
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,9 @@

std::future<void> disconnectAsync(uintptr_t room_handle, DisconnectReason reason);

// Inject a reconnection / chaos scenario for testing (SimulateScenario).
std::future<void> simulateScenarioAsync(uintptr_t room_handle, SimulateScenario scenario);

// Track APIs
std::future<std::vector<RtcStats>> getTrackStatsAsync(uintptr_t track_handle);

Expand Down Expand Up @@ -219,17 +222,17 @@
bool cancelPendingByAsyncId(AsyncId async_id);

/// Map of listener IDs to listener slots
std::unordered_map<ListenerId, std::shared_ptr<ListenerSlot>> listeners_;

Check warning on line 225 in src/ffi_client.h

View workflow job for this annotation

GitHub Actions / Builds / Build (windows-x64)

'livekit::FfiClient::listeners_': 'std::unordered_map<livekit::FfiClient::ListenerId,std::shared_ptr<livekit::FfiClient::ListenerSlot>,std::hash<int>,std::equal_to<livekit::FfiClient::ListenerId>,std::allocator<std::pair<const livekit::FfiClient::ListenerId,std::shared_ptr<livekit::FfiClient::ListenerSlot>>>>' needs to have dll-interface to be used by clients of 'livekit::FfiClient' [D:\a\client-sdk-cpp\client-sdk-cpp\build-release\livekit.vcxproj]

Check warning on line 225 in src/ffi_client.h

View workflow job for this annotation

GitHub Actions / Builds / Build (windows-x64)

'livekit::FfiClient::listeners_': 'std::unordered_map<livekit::FfiClient::ListenerId,std::shared_ptr<livekit::FfiClient::ListenerSlot>,std::hash<int>,std::equal_to<livekit::FfiClient::ListenerId>,std::allocator<std::pair<const livekit::FfiClient::ListenerId,std::shared_ptr<livekit::FfiClient::ListenerSlot>>>>' needs to have dll-interface to be used by clients of 'livekit::FfiClient' [D:\a\client-sdk-cpp\client-sdk-cpp\build-release\livekit.vcxproj]

Check warning on line 225 in src/ffi_client.h

View workflow job for this annotation

GitHub Actions / Tests / Test (windows-x64)

'livekit::FfiClient::listeners_': 'std::unordered_map<livekit::FfiClient::ListenerId,std::shared_ptr<livekit::FfiClient::ListenerSlot>,std::hash<int>,std::equal_to<livekit::FfiClient::ListenerId>,std::allocator<std::pair<const livekit::FfiClient::ListenerId,std::shared_ptr<livekit::FfiClient::ListenerSlot>>>>' needs to have dll-interface to be used by clients of 'livekit::FfiClient' [D:\a\client-sdk-cpp\client-sdk-cpp\build-release\livekit.vcxproj]

Check warning on line 225 in src/ffi_client.h

View workflow job for this annotation

GitHub Actions / Tests / Test (windows-x64)

'livekit::FfiClient::listeners_': 'std::unordered_map<livekit::FfiClient::ListenerId,std::shared_ptr<livekit::FfiClient::ListenerSlot>,std::hash<int>,std::equal_to<livekit::FfiClient::ListenerId>,std::allocator<std::pair<const livekit::FfiClient::ListenerId,std::shared_ptr<livekit::FfiClient::ListenerSlot>>>>' needs to have dll-interface to be used by clients of 'livekit::FfiClient' [D:\a\client-sdk-cpp\client-sdk-cpp\build-release\livekit.vcxproj]
/// Next listener ID to generate
std::atomic<ListenerId> next_listener_id{1};

Check warning on line 227 in src/ffi_client.h

View workflow job for this annotation

GitHub Actions / Builds / Build (windows-x64)

'livekit::FfiClient::next_listener_id': 'std::atomic<int>' needs to have dll-interface to be used by clients of 'livekit::FfiClient' [D:\a\client-sdk-cpp\client-sdk-cpp\build-release\livekit.vcxproj]

Check warning on line 227 in src/ffi_client.h

View workflow job for this annotation

GitHub Actions / Tests / Test (windows-x64)

'livekit::FfiClient::next_listener_id': 'std::atomic<int>' needs to have dll-interface to be used by clients of 'livekit::FfiClient' [D:\a\client-sdk-cpp\client-sdk-cpp\build-release\livekit.vcxproj]
mutable std::mutex lock_;
/// Map of async IDs to pending operations
mutable std::unordered_map<AsyncId, std::unique_ptr<PendingBase>> pending_by_id_;

Check warning on line 230 in src/ffi_client.h

View workflow job for this annotation

GitHub Actions / Builds / Build (windows-x64)

'livekit::FfiClient::pending_by_id_': 'std::unordered_map<livekit::FfiClient::AsyncId,std::unique_ptr<livekit::FfiClient::PendingBase,std::default_delete<livekit::FfiClient::PendingBase>>,std::hash<livekit::FfiClient::AsyncId>,std::equal_to<livekit::FfiClient::AsyncId>,std::allocator<std::pair<const livekit::FfiClient::AsyncId,std::unique_ptr<livekit::FfiClient::PendingBase,std::default_delete<livekit::FfiClient::PendingBase>>>>>' needs to have dll-interface to be used by clients of 'livekit::FfiClient' [D:\a\client-sdk-cpp\client-sdk-cpp\build-release\livekit.vcxproj]

Check warning on line 230 in src/ffi_client.h

View workflow job for this annotation

GitHub Actions / Tests / Test (windows-x64)

'livekit::FfiClient::pending_by_id_': 'std::unordered_map<livekit::FfiClient::AsyncId,std::unique_ptr<livekit::FfiClient::PendingBase,std::default_delete<livekit::FfiClient::PendingBase>>,std::hash<livekit::FfiClient::AsyncId>,std::equal_to<livekit::FfiClient::AsyncId>,std::allocator<std::pair<const livekit::FfiClient::AsyncId,std::unique_ptr<livekit::FfiClient::PendingBase,std::default_delete<livekit::FfiClient::PendingBase>>>>>' needs to have dll-interface to be used by clients of 'livekit::FfiClient' [D:\a\client-sdk-cpp\client-sdk-cpp\build-release\livekit.vcxproj]
/// Next async ID to generate
std::atomic<AsyncId> next_async_id_{1};

Check warning on line 232 in src/ffi_client.h

View workflow job for this annotation

GitHub Actions / Builds / Build (windows-x64)

'livekit::FfiClient::next_async_id_': 'std::atomic<unsigned __int64>' needs to have dll-interface to be used by clients of 'livekit::FfiClient' [D:\a\client-sdk-cpp\client-sdk-cpp\build-release\livekit.vcxproj]

Check warning on line 232 in src/ffi_client.h

View workflow job for this annotation

GitHub Actions / Tests / Test (windows-x64)

'livekit::FfiClient::next_async_id_': 'std::atomic<unsigned __int64>' needs to have dll-interface to be used by clients of 'livekit::FfiClient' [D:\a\client-sdk-cpp\client-sdk-cpp\build-release\livekit.vcxproj]

void pushEvent(const proto::FfiEvent& event) const;
friend void ffiEventCallback(const uint8_t* buf, size_t len);
std::atomic<LifecycleState> lifecycle_state_{LifecycleState::Uninitialized};

Check warning on line 236 in src/ffi_client.h

View workflow job for this annotation

GitHub Actions / Builds / Build (windows-x64)

'livekit::FfiClient::lifecycle_state_': 'std::atomic<livekit::FfiClient::LifecycleState>' needs to have dll-interface to be used by clients of 'livekit::FfiClient' [D:\a\client-sdk-cpp\client-sdk-cpp\build-release\livekit.vcxproj]

Check warning on line 236 in src/ffi_client.h

View workflow job for this annotation

GitHub Actions / Tests / Test (windows-x64)

'livekit::FfiClient::lifecycle_state_': 'std::atomic<livekit::FfiClient::LifecycleState>' needs to have dll-interface to be used by clients of 'livekit::FfiClient' [D:\a\client-sdk-cpp\client-sdk-cpp\build-release\livekit.vcxproj]
};
} // namespace livekit
24 changes: 24 additions & 0 deletions src/room.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,30 @@ bool Room::disconnect(DisconnectReason reason) {
return ffi_ok;
}

bool Room::simulateScenario(SimulateScenario scenario) {
TRACE_EVENT0("livekit", "Room::simulateScenario");

std::shared_ptr<FfiHandle> handle;
{
const std::scoped_lock<std::mutex> g(lock_);
if (connection_state_ != ConnectionState::Connected) {
throw std::runtime_error("Room::simulateScenario called on a room that is not connected");
}
handle = room_handle_;
}
if (!handle) {
throw std::runtime_error("Room::simulateScenario: missing room handle");
}

try {
FfiClient::instance().simulateScenarioAsync(handle->get(), scenario).get();
return true;
} catch (const std::exception& e) {
LK_LOG_ERROR("Room::simulateScenario failed: {}", e.what());
return false;
}
}

RoomInfoData Room::roomInfo() const {
const std::scoped_lock<std::mutex> g(lock_);
return room_info_;
Expand Down
25 changes: 25 additions & 0 deletions src/room_proto_converter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,31 @@ proto::DisconnectReason toProto(DisconnectReason in) {
}
}

proto::SimulateScenarioKind toProto(SimulateScenario in) {
switch (in) {
case SimulateScenario::SignalReconnect:
return proto::SIMULATE_SIGNAL_RECONNECT;
case SimulateScenario::Speaker:
return proto::SIMULATE_SPEAKER;
case SimulateScenario::NodeFailure:
return proto::SIMULATE_NODE_FAILURE;
case SimulateScenario::ServerLeave:
return proto::SIMULATE_SERVER_LEAVE;
case SimulateScenario::Migration:
return proto::SIMULATE_MIGRATION;
case SimulateScenario::ForceTcp:
return proto::SIMULATE_FORCE_TCP;
case SimulateScenario::ForceTls:
return proto::SIMULATE_FORCE_TLS;
case SimulateScenario::FullReconnect:
return proto::SIMULATE_FULL_RECONNECT;
case SimulateScenario::DisconnectSignalOnResume:
return proto::SIMULATE_DISCONNECT_SIGNAL_ON_RESUME;
default:
return proto::SIMULATE_SIGNAL_RECONNECT;
}
}

// --------- basic helper conversions ---------

UserPacketData fromProto(const proto::UserPacket& in) {
Expand Down
1 change: 1 addition & 0 deletions src/room_proto_converter.h
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ LIVEKIT_INTERNAL_API ConnectionState toConnectionState(proto::ConnectionState in
LIVEKIT_INTERNAL_API DataPacketKind toDataPacketKind(proto::DataPacketKind in);
LIVEKIT_INTERNAL_API DisconnectReason toDisconnectReason(proto::DisconnectReason in);
LIVEKIT_INTERNAL_API proto::DisconnectReason toProto(DisconnectReason in);
LIVEKIT_INTERNAL_API proto::SimulateScenarioKind toProto(SimulateScenario in);

LIVEKIT_INTERNAL_API UserPacketData fromProto(const proto::UserPacket& in);
LIVEKIT_INTERNAL_API SipDtmfData fromProto(const proto::SipDTMF& in);
Expand Down
Loading
Loading