From 4163172c36220bba665b7ab44f436be2837421b3 Mon Sep 17 00:00:00 2001 From: shijing xian Date: Fri, 5 Dec 2025 11:36:40 -0800 Subject: [PATCH 01/12] RPC features --- CMakeLists.txt | 2 + README.md | 37 +- examples/CMakeLists.txt | 21 + examples/simple_room/main.cpp | 12 +- examples/simple_rpc/main.cpp | 572 ++++++++++++++++++++++++++++ include/livekit/ffi_client.h | 5 + include/livekit/local_participant.h | 95 ++++- include/livekit/rpc_error.h | 123 ++++++ src/ffi_client.cpp | 40 ++ src/local_participant.cpp | 99 +++++ src/room.cpp | 31 +- src/room_proto_converter.cpp | 1 + src/room_proto_converter.h | 4 + src/rpc_error.cpp | 91 +++++ 14 files changed, 1116 insertions(+), 17 deletions(-) create mode 100644 examples/simple_rpc/main.cpp create mode 100644 include/livekit/rpc_error.h create mode 100644 src/rpc_error.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 6baabde..473fd49 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -169,6 +169,7 @@ add_library(livekit include/livekit/local_participant.h include/livekit/remote_participant.h include/livekit/livekit.h + include/livekit/rpc_error.h include/livekit/stats.h include/livekit/track.h include/livekit/track_publication.h @@ -198,6 +199,7 @@ add_library(livekit src/track_publication.cpp src/local_track_publication.cpp src/remote_track_publication.cpp + src/rpc_error.cpp src/video_frame.cpp src/video_source.cpp src/video_stream.cpp diff --git a/README.md b/README.md index c294f8f..cc55a3a 100644 --- a/README.md +++ b/README.md @@ -41,8 +41,16 @@ All build actions are managed by the provided build.sh script. ## πŸ§ͺ Run Example +### Generate Tokens +Before running any participant, create JWT tokens with the proper identity and room name, example ```bash -./build/examples/SimpleRoom --url ws://localhost:7880 --token +lk token create -r test -i your_own_identity --join --valid-for 99999h --dev --room=your_own_room +``` + +### SimpleRoom + +```bash +./build/examples/SimpleRoom --url $URL --token ``` You can also provide the URL and token via environment variables: @@ -54,6 +62,33 @@ export LIVEKIT_TOKEN= Press Ctrl-C to exit the example. +### SimpleRpc +The SimpleRpc example demonstrates how to: +- Connect multiple participants to the same LiveKit room +- Register RPC handlers (e.g., arrival, square-root, divide, long-calculation) +- Send RPC requests from one participant to another +- Handle success, application errors, unsupported methods, and timeouts +- Observe round-trip times (RTT) for each RPC call + +#### πŸ”‘ Generate Tokens +Before running any participant, create JWT tokens with "caller", "greeter" and "math-genius" identities and room name. +```bash +lk token create -r test -i caller --join --valid-for 99999h --dev --room=your_own_room +lk token create -r test -i greeter --join --valid-for 99999h --dev --room=your_own_room +lk token create -r test -i math-genius --join --valid-for 99999h --dev --room=your_own_room +``` + +#### β–Ά Start Participants +Every participant is run as a separate terminal process, note --role needs to match the token identity. +```bash +./build/examples/SimpleRpc --url $URL --token --role=math-genius +``` +The caller will automatically: +- Wait for the greeter and math-genius to join +- Perform RPC calls +- Print round-trip times +- Annotate expected successes or expected failures + ## 🧰 Recommended Setup ### macOS ```bash diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index f242af3..39e223e 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -4,6 +4,8 @@ project (livekit-examples) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) +#################### SimpleRoom example ########################## + list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") include(sdl3) @@ -32,3 +34,22 @@ add_custom_command(TARGET SimpleRoom POST_BUILD ${CMAKE_SOURCE_DIR}/data ${CMAKE_CURRENT_BINARY_DIR}/data ) + +#################### SimpleRpc example ########################## + +include(FetchContent) +FetchContent_Declare( + nlohmann_json + URL https://github.com/nlohmann/json/releases/download/v3.11.3/json.tar.xz +) +FetchContent_MakeAvailable(nlohmann_json) + +add_executable(SimpleRpc + simple_rpc/main.cpp +) + +target_link_libraries(SimpleRpc + PRIVATE + nlohmann_json::nlohmann_json + livekit +) \ No newline at end of file diff --git a/examples/simple_room/main.cpp b/examples/simple_room/main.cpp index e675a2f..9338e2f 100644 --- a/examples/simple_room/main.cpp +++ b/examples/simple_room/main.cpp @@ -42,7 +42,7 @@ namespace { std::atomic g_running{true}; -void print_usage(const char *prog) { +void printUsage(const char *prog) { std::cerr << "Usage:\n" << " " << prog << " \n" << "or:\n" @@ -52,9 +52,9 @@ void print_usage(const char *prog) { << " LIVEKIT_URL, LIVEKIT_TOKEN\n"; } -void handle_sigint(int) { g_running.store(false); } +void handleSignal(int) { g_running.store(false); } -bool parse_args(int argc, char *argv[], std::string &url, std::string &token) { +bool parseArgs(int argc, char *argv[], std::string &url, std::string &token) { // 1) --help for (int i = 1; i < argc; ++i) { std::string a = argv[i]; @@ -215,8 +215,8 @@ class SimpleRoomDelegate : public livekit::RoomDelegate { int main(int argc, char *argv[]) { std::string url, token; - if (!parse_args(argc, argv, url, token)) { - print_usage(argv[0]); + if (!parseArgs(argc, argv, url, token)) { + printUsage(argv[0]); return 1; } @@ -238,7 +238,7 @@ int main(int argc, char *argv[]) { std::cout << "Connecting to: " << url << std::endl; // Handle Ctrl-C to exit the idle loop - std::signal(SIGINT, handle_sigint); + std::signal(SIGINT, handleSignal); livekit::Room room{}; SimpleRoomDelegate delegate(media); diff --git a/examples/simple_rpc/main.cpp b/examples/simple_rpc/main.cpp new file mode 100644 index 0000000..4686bb5 --- /dev/null +++ b/examples/simple_rpc/main.cpp @@ -0,0 +1,572 @@ +/* + * Copyright 2025 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. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "livekit/livekit.h" +#include "livekit_ffi.h" // same as simple_room; internal but used here + +using namespace livekit; +using namespace std::chrono_literals; + +namespace { + +// ------------------------------------------------------------ +// Global control (same pattern as simple_room) +// ------------------------------------------------------------ + +std::atomic g_running{true}; + +void handleSignal(int) { g_running.store(false); } + +void printUsage(const char *prog) { + std::cerr << "Usage:\n" + << " " << prog << " [role]\n" + << "or:\n" + << " " << prog + << " --url= --token= [--role=]\n" + << " " << prog + << " --url --token [--role ]\n\n" + << "Env fallbacks:\n" + << " LIVEKIT_URL, LIVEKIT_TOKEN\n" + << "Role (participant behavior):\n" + << " SIMPLE_RPC_ROLE or --role=\n" + << " default: caller\n"; +} + +inline double nowMs() { + return std::chrono::duration( + std::chrono::steady_clock::now().time_since_epoch()) + .count(); +} + +// Poll the room until a remote participant with the given identity appears, +// or until 'timeout' elapses. Returns true if found, false on timeout. +bool waitForParticipant(Room &room, const std::string &identity, + std::chrono::milliseconds timeout) { + auto start = std::chrono::steady_clock::now(); + + while (std::chrono::steady_clock::now() - start < timeout) { + if (room.remote_participant(identity) != nullptr) { + return true; + } + std::this_thread::sleep_for(100ms); + } + return false; +} + +// For the caller: wait for a specific peer, and if they don't show up, +// explain why and how to start them in another terminal. +bool ensurePeerPresent(Room &room, const std::string &identity, + const std::string &friendly_role, const std::string &url, + std::chrono::seconds timeout) { + std::cout << "[Caller] Waiting up to " << timeout.count() << "s for " + << friendly_role << " (identity=\"" << identity + << "\") to join...\n"; + + bool present = waitForParticipant( + room, identity, + std::chrono::duration_cast(timeout)); + + if (present) { + std::cout << "[Caller] " << friendly_role << " is present.\n"; + return true; + } + + // Timed out + auto info = room.room_info(); + const std::string room_name = info.name; + + std::cout << "[Caller] Timed out after " << timeout.count() + << "s waiting for " << friendly_role << " (identity=\"" << identity + << "\").\n"; + std::cout << "[Caller] No participant with identity \"" << identity + << "\" appears to be connected to room \"" << room_name + << "\".\n\n"; + + std::cout << "To start a " << friendly_role + << " in another terminal, run:\n\n" + << " lk token create -r test -i " << identity + << " --join --valid-for 99999h --dev --room=" << room_name << "\n" + << " ./build/examples/SimpleRpc " << url + << " $token --role=" << friendly_role << "\n\n"; + + return false; +} + +// Parse args similar to simple_room, plus optional --role / role positional +bool parseArgs(int argc, char *argv[], std::string &url, std::string &token, + std::string &role) { + // --help + for (int i = 1; i < argc; ++i) { + std::string a = argv[i]; + if (a == "-h" || a == "--help") { + return false; + } + } + + // helper for flags + auto get_flag_value = [&](const std::string &name, int &i) -> std::string { + std::string arg = argv[i]; + const std::string eq = name + "="; + if (arg.rfind(name, 0) == 0) { // starts with name + if (arg.size() > name.size() && arg[name.size()] == '=') { + return arg.substr(eq.size()); + } else if (i + 1 < argc) { + return std::string(argv[++i]); + } + } + return {}; + }; + + // flags: --url / --token / --role (with = or split) + for (int i = 1; i < argc; ++i) { + const std::string a = argv[i]; + if (a.rfind("--url", 0) == 0) { + auto v = get_flag_value("--url", i); + if (!v.empty()) + url = v; + } else if (a.rfind("--token", 0) == 0) { + auto v = get_flag_value("--token", i); + if (!v.empty()) + token = v; + } else if (a.rfind("--role", 0) == 0) { + auto v = get_flag_value("--role", i); + if (!v.empty()) + role = v; + } + } + + std::vector pos; + for (int i = 1; i < argc; ++i) { + std::string a = argv[i]; + if (a.rfind("--", 0) == 0) + continue; + pos.push_back(std::move(a)); + } + if (!pos.empty()) { + if (url.empty() && pos.size() >= 1) { + url = pos[0]; + } + if (token.empty() && pos.size() >= 2) { + token = pos[1]; + } + if (role.empty() && pos.size() >= 3) { + role = pos[2]; + } + } + + if (url.empty()) { + const char *e = std::getenv("LIVEKIT_URL"); + if (e) + url = e; + } + if (token.empty()) { + const char *e = std::getenv("LIVEKIT_TOKEN"); + if (e) + token = e; + } + if (role.empty()) { + const char *e = std::getenv("SIMPLE_RPC_ROLE"); + if (e) + role = e; + } + if (role.empty()) { + role = "caller"; + } + + return !(url.empty() || token.empty()); +} + +// ------------------------------------------------------------ +// Tiny helpers for the simple JSON used in the sample +// (to avoid bringing in a json library) +// ------------------------------------------------------------ + +// create {"key":number} +std::string makeNumberJson(const std::string &key, double value) { + std::ostringstream oss; + oss << "{\"" << key << "\":" << value << "}"; + return oss.str(); +} + +// create {"key":"value"} +std::string makeStringJson(const std::string &key, const std::string &value) { + std::ostringstream oss; + oss << "{\"" << key << "\":\"" << value << "\"}"; + return oss.str(); +} + +// very naive parse of {"key":number} +double parseNumberFromJson(const std::string &json) { + auto colon = json.find(':'); + if (colon == std::string::npos) + throw std::runtime_error("invalid json: " + json); + auto start = colon + 1; + auto end = json.find_first_of(",}", start); + std::string num_str = json.substr(start, end - start); + return std::stod(num_str); +} + +// very naive parse of {"key":"value"} +std::string parseStringFromJson(const std::string &json) { + auto colon = json.find(':'); + if (colon == std::string::npos) + throw std::runtime_error("invalid json: " + json); + auto first_quote = json.find('"', colon + 1); + if (first_quote == std::string::npos) + throw std::runtime_error("invalid json: " + json); + auto second_quote = json.find('"', first_quote + 1); + if (second_quote == std::string::npos) + throw std::runtime_error("invalid json: " + json); + return json.substr(first_quote + 1, second_quote - first_quote - 1); +} + +// ------------------------------------------------------------ +// RPC handler registration (for greeter & math-genius) +// ------------------------------------------------------------ + +void registerReceiverMethods(Room &greeters_room, Room &math_genius_room) { + LocalParticipant *greeter_lp = greeters_room.local_participant(); + LocalParticipant *math_genius_lp = math_genius_room.local_participant(); + + // arrival + greeter_lp->registerRpcMethod( + "arrival", + [](const RpcInvocationData &data) -> std::optional { + std::cout << "[Greeter] Oh " << data.caller_identity + << " arrived and said \"" << data.payload << "\"\n"; + std::this_thread::sleep_for(2s); + return std::optional{"Welcome and have a wonderful day!"}; + }); + + // square-root + math_genius_lp->registerRpcMethod( + "square-root", + [](const RpcInvocationData &data) -> std::optional { + double number = parseNumberFromJson(data.payload); + std::cout << "[Math Genius] I guess " << data.caller_identity + << " wants the square root of " << number + << ". I've only got " << data.response_timeout_sec + << " seconds to respond but I think I can pull it off.\n"; + std::cout << "[Math Genius] *doing math*…\n"; + std::this_thread::sleep_for(2s); + double result = std::sqrt(number); + std::cout << "[Math Genius] Aha! It's " << result << "\n"; + return makeNumberJson("result", result); + }); + + // divide + math_genius_lp->registerRpcMethod( + "divide", + [](const RpcInvocationData &data) -> std::optional { + // expect {"dividend":X,"divisor":Y} – we'll parse very lazily + auto div_pos = data.payload.find("dividend"); + auto dvr_pos = data.payload.find("divisor"); + if (div_pos == std::string::npos || dvr_pos == std::string::npos) { + throw std::runtime_error("invalid divide payload"); + } + + double dividend = parseNumberFromJson( + data.payload.substr(div_pos, dvr_pos - div_pos - 1)); // rough slice + double divisor = parseNumberFromJson(data.payload.substr(dvr_pos)); + + std::cout << "[Math Genius] " << data.caller_identity + << " wants to divide " << dividend << " by " << divisor + << ".\n"; + + if (divisor == 0.0) { + // will be translated to APPLICATION_ERROR by your RpcError logic + throw std::runtime_error("division by zero"); + } + + double result = dividend / divisor; + return makeNumberJson("result", result); + }); + + // long-calculation + math_genius_lp->registerRpcMethod( + "long-calculation", + [](const RpcInvocationData &data) -> std::optional { + std::cout << "[Math Genius] Starting a very long calculation for " + << data.caller_identity << "\n"; + std::cout << "[Math Genius] This will take 30 seconds even though " + "you're only giving me " + << data.response_timeout_sec << " seconds\n"; + + std::this_thread::sleep_for(30s); + return makeStringJson("result", "Calculation complete!"); + }); + + // Note: we do NOT register "quantum-hypergeometric-series" here, + // so the caller sees UNSUPPORTED_METHOD, just like in Python. +} + +// ------------------------------------------------------------ +// Caller-side helpers (like perform_* in rpc.py) +// ------------------------------------------------------------ + +void performGreeting(Room &room) { + std::cout << "[Caller] Letting the greeter know that I've arrived\n"; + double t0 = nowMs(); + try { + std::string response = room.local_participant()->performRpc( + "greeter", "arrival", "Hello", std::nullopt); + double t1 = nowMs(); + std::cout << "[Caller] RTT: " << (t1 - t0) << " ms\n"; + std::cout << "[Caller] That's nice, the greeter said: \"" << response + << "\"\n"; + } catch (const std::exception &error) { + double t1 = nowMs(); + std::cout << "[Caller] (FAILED) RTT: " << (t1 - t0) << " ms\n"; + std::cout << "[Caller] RPC call failed: " << error.what() << "\n"; + throw; + } +} + +void performSquareRoot(Room &room) { + std::cout << "[Caller] What's the square root of 16?\n"; + double t0 = nowMs(); + try { + std::string payload = makeNumberJson("number", 16.0); + std::string response = room.local_participant()->performRpc( + "math-genius", "square-root", payload, std::nullopt); + double t1 = nowMs(); + std::cout << "[Caller] RTT: " << (t1 - t0) << " ms\n"; + double result = parseNumberFromJson(response); + std::cout << "[Caller] Nice, the answer was " << result << "\n"; + } catch (const std::exception &error) { + double t1 = nowMs(); + std::cout << "[Caller] (FAILED) RTT: " << (t1 - t0) << " ms\n"; + std::cout << "[Caller] RPC call failed: " << error.what() << "\n"; + throw; + } +} + +void performQuantumHyperGeometricSeries(Room &room) { + std::cout << "\n=== Unsupported Method Example ===\n"; + std::cout + << "[Caller] Asking math-genius for 'quantum-hypergeometric-series'. " + "This should FAIL because the handler is NOT registered.\n"; + double t0 = nowMs(); + try { + std::string payload = makeNumberJson("number", 42.0); + std::string response = room.local_participant()->performRpc( + "math-genius", "quantum-hypergeometric-series", payload, std::nullopt); + double t1 = nowMs(); + std::cout << "[Caller] (Unexpected success) RTT=" << (t1 - t0) << " ms\n"; + std::cout << "[Caller] Result: " << response << "\n"; + } catch (const RpcError &error) { + double t1 = nowMs(); + std::cout << "[Caller] RpcError RTT=" << (t1 - t0) << " ms\n"; + auto code = static_cast(error.code()); + if (code == RpcError::ErrorCode::UNSUPPORTED_METHOD) { + std::cout << "[Caller] βœ“ Expected: math-genius does NOT implement this " + "method.\n"; + std::cout << "[Caller] Server returned UNSUPPORTED_METHOD.\n"; + } else { + std::cout << "[Caller] βœ— Unexpected error type: " << error.message() + << "\n"; + } + } +} + +void performDivide(Room &room) { + std::cout << "\n=== Divide Example ===\n"; + std::cout << "[Caller] Asking math-genius to divide 10 by 0. " + "This is EXPECTED to FAIL with an APPLICATION_ERROR.\n"; + double t0 = nowMs(); + try { + std::string payload = "{\"dividend\":10,\"divisor\":0}"; + std::string response = room.local_participant()->performRpc( + "math-genius", "divide", payload, std::nullopt); + double t1 = nowMs(); + std::cout << "[Caller] (Unexpected success) RTT=" << (t1 - t0) << " ms\n"; + std::cout << "[Caller] Result = " << response << "\n"; + } catch (const RpcError &error) { + double t1 = nowMs(); + std::cout << "[Caller] RpcError RTT=" << (t1 - t0) << " ms\n"; + auto code = static_cast(error.code()); + if (code == RpcError::ErrorCode::APPLICATION_ERROR) { + std::cout << "[Caller] βœ“ Expected: divide-by-zero triggers " + "APPLICATION_ERROR.\n"; + std::cout << "[Caller] Math-genius threw an exception: " + << error.message() << "\n"; + } else { + std::cout << "[Caller] βœ— Unexpected RpcError type: " << error.message() + << "\n"; + } + } +} + +void performLongCalculation(Room &room) { + std::cout << "\n=== Long Calculation Example ===\n"; + std::cout + << "[Caller] Asking math-genius for a calculation that takes 30s.\n"; + std::cout + << "[Caller] Giving only 10s to respond. EXPECTED RESULT: TIMEOUT.\n"; + double t0 = nowMs(); + try { + std::string response = room.local_participant()->performRpc( + "math-genius", "long-calculation", "{}", 10.0); + double t1 = nowMs(); + std::cout << "[Caller] (Unexpected success) RTT=" << (t1 - t0) << " ms\n"; + std::cout << "[Caller] Result: " << response << "\n"; + } catch (const RpcError &error) { + double t1 = nowMs(); + std::cout << "[Caller] RpcError RTT=" << (t1 - t0) << " ms\n"; + auto code = static_cast(error.code()); + if (code == RpcError::ErrorCode::RESPONSE_TIMEOUT) { + std::cout + << "[Caller] βœ“ Expected: handler sleeps 30s but timeout is 10s.\n"; + std::cout << "[Caller] Server correctly returned RESPONSE_TIMEOUT.\n"; + } else if (code == RpcError::ErrorCode::RECIPIENT_DISCONNECTED) { + std::cout << "[Caller] βœ“ Expected if math-genius disconnects during the " + "test.\n"; + } else { + std::cout << "[Caller] βœ— Unexpected RPC error: " << error.message() + << "\n"; + } + } +} + +} // namespace + +// ------------------------------------------------------------ +// main – similar style to simple_room/main.cpp +// ------------------------------------------------------------ + +int main(int argc, char *argv[]) { + std::string url, token, role; + if (!parseArgs(argc, argv, url, token, role)) { + printUsage(argv[0]); + return 1; + } + + if (url.empty() || token.empty()) { + std::cerr << "LIVEKIT_URL and LIVEKIT_TOKEN (or CLI args) are required\n"; + return 1; + } + + std::cout << "Connecting to: " << url << "\n"; + std::cout << "Role: " << role << "\n"; + + // Ctrl-C + std::signal(SIGINT, handleSignal); + + Room room{}; + RoomOptions options; + options.auto_subscribe = true; + options.dynacast = false; + + bool res = room.Connect(url, token, options); + std::cout << "Connect result is " << std::boolalpha << res << "\n"; + if (!res) { + std::cerr << "Failed to connect to room\n"; + FfiClient::instance().shutdown(); + return 1; + } + + auto info = room.room_info(); + std::cout << "Connected to room:\n" + << " Name: " << info.name << "\n" + << " Metadata: " << info.metadata << "\n" + << " Num participants: " << info.num_participants << "\n"; + + try { + if (role == "caller") { + // Check that both peers are present (or explain how to start them). + bool has_greeter = ensurePeerPresent(room, "greeter", "greeter", url, 8s); + bool has_math_genius = + ensurePeerPresent(room, "math-genius", "math-genius", url, 8s); + if (!has_greeter || !has_math_genius) { + std::cout << "\n[Caller] One or more RPC peers are missing. " + << "Some examples may be skipped.\n"; + } + if (has_greeter) { + std::cout << "\n\nRunning greeting example...\n"; + performGreeting(room); + } else { + std::cout << "[Caller] Skipping greeting example because greeter is " + "not present.\n"; + } + if (has_math_genius) { + std::cout << "\n\nRunning error handling example...\n"; + performDivide(room); + + std::cout << "\n\nRunning math example...\n"; + performSquareRoot(room); + std::this_thread::sleep_for(2s); + performQuantumHyperGeometricSeries(room); + + std::cout << "\n\nRunning long calculation with timeout...\n"; + performLongCalculation(room); + } else { + std::cout << "[Caller] Skipping math examples because math-genius is " + "not present.\n"; + } + + std::cout << "\n\nCaller done. Exiting.\n"; + } else if (role == "greeter" || role == "math-genius") { + // For these roles we expect multiple processes: + // - One process with role=caller + // - One with role=greeter + // - One with role=math-genius + // + // Each process gets its own token (with that identity) via LIVEKIT_TOKEN. + // Here we only register handlers for the appropriate role, and then + // stay alive until Ctrl-C so we can receive RPCs. + + if (role == "greeter") { + // Use the same room object for both arguments; only "arrival" is used. + registerReceiverMethods(room, room); + } else { // math-genius + // We only need math handlers; greeter handlers won't be used. + registerReceiverMethods(room, room); + } + + std::cout << "RPC handlers registered for role=" << role + << ". Waiting for RPC calls (Ctrl-C to exit)...\n"; + + while (g_running.load()) { + std::this_thread::sleep_for(50ms); + } + std::cout << "Exiting receiver role.\n"; + } else { + std::cerr << "Unknown role: " << role << "\n"; + } + } catch (const std::exception &e) { + std::cerr << "Unexpected error in main: " << e.what() << "\n"; + } + + FfiClient::instance().shutdown(); + return 0; +} diff --git a/include/livekit/ffi_client.h b/include/livekit/ffi_client.h index 51cd226..6b2fa3d 100644 --- a/include/livekit/ffi_client.h +++ b/include/livekit/ffi_client.h @@ -111,6 +111,11 @@ class FfiClient { std::future captureAudioFrameAsync(std::uint64_t source_handle, const proto::AudioFrameBufferInfo &buffer); + std::future performRpcAsync( + std::uint64_t local_participant_handle, + const std::string &destination_identity, const std::string &method, + const std::string &payload, + std::optional response_timeout_ms = std::nullopt); // Generic function for sending a request to the Rust FFI. // Note: For asynchronous requests, use the dedicated async functions instead diff --git a/include/livekit/local_participant.h b/include/livekit/local_participant.h index 1077d73..28aa70a 100644 --- a/include/livekit/local_participant.h +++ b/include/livekit/local_participant.h @@ -19,6 +19,7 @@ #include "livekit/ffi_handle.h" #include "livekit/participant.h" #include "livekit/room_delegate.h" +#include "livekit/rpc_error.h" #include #include @@ -33,8 +34,16 @@ struct ParticipantTrackPermission; class FfiClient; class Track; class LocalTrackPublication; +// TODO, should consider moving Transcription to local_participant.h? struct Transcription; +struct RpcInvocationData { + std::string request_id; + std::string caller_identity; + std::string payload; + double response_timeout_sec; // seconds +}; + /** * Represents the local participant in a room. * @@ -45,6 +54,19 @@ class LocalParticipant : public Participant { using PublicationMap = std::unordered_map>; + /** + * Type of callback used to handle incoming RPC method invocations. + * + * The handler receives an RpcInvocationData describing the incoming call + * and may return an optional response payload. To signal an error to the + * remote caller, throw an RpcError; it will be serialized and forwarded. + * + * Returning std::nullopt means "no payload" and results in an empty + * response body being sent back to the caller. + */ + using RpcHandler = + std::function(const RpcInvocationData &)>; + LocalParticipant(FfiHandle handle, std::string sid, std::string name, std::string identity, std::string metadata, std::unordered_map attributes, @@ -91,10 +113,6 @@ class LocalParticipant : public Participant { void setAttributes(const std::unordered_map &attributes); - // ------------------------------------------------------------------------- - // Subscription permissions - // ------------------------------------------------------------------------- - /** * Set track subscription permissions for this participant. * @@ -106,10 +124,6 @@ class LocalParticipant : public Participant { const std::vector &participant_permissions = {}); - // ------------------------------------------------------------------------- - // Track publish / unpublish (synchronous analogue) - // ------------------------------------------------------------------------- - /** * Publish a local track to the room. * @@ -126,8 +140,73 @@ class LocalParticipant : public Participant { */ void unpublishTrack(const std::string &track_sid); + /** + * Initiate an RPC call to a remote participant. + * + * @param destination_identity Identity of the destination participant. + * @param method Name of the RPC method to invoke. + * @param payload Request payload to send to the remote handler. + * @param response_timeout Optional timeout in seconds for receiving + * a response. If not set, the server default + * timeout (15 seconds) is used. + * + * @return The response payload returned by the remote handler. + * + * @throws RpcError If the remote side returns an RPC error, times out, + * or rejects the request. + * @throws std::runtime_error If the underlying FFI handle is invalid or + * the FFI call fails unexpectedly. + */ + std::string performRpc(const std::string &destination_identity, + const std::string &method, const std::string &payload, + std::optional response_timeout = std::nullopt); + + /** + * Register a handler for an incoming RPC method. + * + * Once registered, the provided handler will be invoked whenever a remote + * participant calls the given method name on this LocalParticipant. + * + * @param method_name Name of the RPC method to handle. This must match + * the method name used by remote callers. + * @param handler Callback to execute when an invocation is received. + * The handler may return an optional response payload + * or throw an RpcError to signal failure. + * + * If a handler is already registered for the same method_name, it will be + * replaced by the new handler. + */ + + void registerRpcMethod(const std::string &method_name, RpcHandler handler); + + /** + * Unregister a previously registered RPC method handler. + * + * After this call, invocations for the given method_name will no longer + * be dispatched to a local handler and will instead result in an + * "unsupported method" error being returned to the caller. + * + * @param method_name Name of the RPC method to unregister. + * If no handler is registered for this name, the call + * is a no-op. + */ + void unregisterRpcMethod(const std::string &method_name); + +protected: + // Called by Room when an rpc_method_invocation event is received from the + // SFU. This is internal plumbing and not intended to be called directly by + // SDK users. + void handleRpcMethodInvocation(std::uint64_t invocation_id, + const std::string &method, + const std::string &request_id, + const std::string &caller_identity, + const std::string &payload, + double response_timeout); + friend class Room; + private: PublicationMap track_publications_; + std::unordered_map rpc_handlers_; }; } // namespace livekit diff --git a/include/livekit/rpc_error.h b/include/livekit/rpc_error.h new file mode 100644 index 0000000..0fd7195 --- /dev/null +++ b/include/livekit/rpc_error.h @@ -0,0 +1,123 @@ +/* + * Copyright 2025 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 +#include +#include + +namespace livekit { + +namespace proto { +class RpcError; +} + +/** + * Specialized error type for RPC methods. + * + * Instances of this type, when thrown in a method handler, will have their + * `code`, `message`, and optional `data` serialized into a proto::RpcError + * and sent across the wire. The caller will receive an equivalent error + * on the other side. + * + * Built-in errors are included (codes 1400–1999) but developers may use + * arbitrary codes as well. + */ +class RpcError : public std::runtime_error { +public: + /** + * Built-in error codes, mirroring the Python RpcError.ErrorCode enum. + */ + enum class ErrorCode : std::uint32_t { + APPLICATION_ERROR = 1500, + CONNECTION_TIMEOUT = 1501, + RESPONSE_TIMEOUT = 1502, + RECIPIENT_DISCONNECTED = 1503, + RESPONSE_PAYLOAD_TOO_LARGE = 1504, + SEND_FAILED = 1505, + + UNSUPPORTED_METHOD = 1400, + RECIPIENT_NOT_FOUND = 1401, + REQUEST_PAYLOAD_TOO_LARGE = 1402, + UNSUPPORTED_SERVER = 1403, + UNSUPPORTED_VERSION = 1404, + }; + + /** + * Construct an RpcError with an explicit numeric code. + * + * @param code Error code value. Codes 1001–1999 are reserved for + * built-in errors (see ErrorCode). + * @param message Human-readable error message. + * @param data Optional extra data, e.g. JSON. Empty string means no data. + */ + RpcError(std::uint32_t code, std::string message, std::string data = {}); + + /** + * Construct an RpcError from a built-in ErrorCode. + * + * @param code Built-in error code. + * @param message Human-readable error message. + * @param data Optional extra data, e.g. JSON. Empty string means no data. + */ + RpcError(ErrorCode code, std::string message, std::string data = {}); + + /** + * Numeric error code. + * + * Codes 1001–1999 are reserved for built-in errors. For built-ins, this + * value matches the underlying ErrorCode enum value. + */ + std::uint32_t code() const noexcept; + + /** + * Human-readable error message. + */ + const std::string &message() const noexcept; + + /** + * Optional extra data associated with the error (JSON recommended). + * May be an empty string if no data was provided. + */ + const std::string &data() const noexcept; + + /** + * Create a built-in RpcError using a predefined ErrorCode and default + * message text that matches the Python RpcError.ErrorMessage table. + * + * @param code Built-in error code. + * @param data Optional extra data payload (JSON recommended). + */ + static RpcError builtIn(ErrorCode code, const std::string &data = {}); + +protected: + // ----- Protected: only used by LocalParticipant (internal SDK code) ----- + proto::RpcError toProto() const; + static RpcError fromProto(const proto::RpcError &err); + + friend class LocalParticipant; + friend class FfiClient; + +private: + static const char *defaultMessageFor(ErrorCode code); + + std::uint32_t code_; + std::string message_; + std::string data_; +}; + +} // namespace livekit \ No newline at end of file diff --git a/src/ffi_client.cpp b/src/ffi_client.cpp index 5b426d0..38bc52d 100644 --- a/src/ffi_client.cpp +++ b/src/ffi_client.cpp @@ -22,6 +22,7 @@ #include "livekit/ffi_client.h" #include "livekit/ffi_handle.h" #include "livekit/room.h" // TODO, maybe avoid circular deps by moving RoomOptions to a room_types.h ? +#include "livekit/rpc_error.h" #include "livekit/track.h" #include "livekit_ffi.h" #include "room.pb.h" @@ -505,4 +506,43 @@ FfiClient::captureAudioFrameAsync(std::uint64_t source_handle, }); } +std::future +FfiClient::performRpcAsync(std::uint64_t local_participant_handle, + const std::string &destination_identity, + const std::string &method, + const std::string &payload, + std::optional response_timeout_ms) { + proto::FfiRequest req; + auto *msg = req.mutable_perform_rpc(); + msg->set_local_participant_handle(local_participant_handle); + msg->set_destination_identity(destination_identity); + msg->set_method(method); + msg->set_payload(payload); + if (response_timeout_ms.has_value()) { + msg->set_response_timeout_ms(*response_timeout_ms); + } + proto::FfiResponse resp = sendRequest(req); + if (!resp.has_perform_rpc()) { + throw std::runtime_error("FfiResponse missing perform_rpc"); + } + const AsyncId async_id = resp.perform_rpc().async_id(); + return registerAsync( + // match predicate + [async_id](const proto::FfiEvent &event) { + return event.has_perform_rpc() && + event.perform_rpc().async_id() == async_id; + }, + [](const proto::FfiEvent &event, std::promise &pr) { + const auto &cb = event.perform_rpc(); + + if (cb.has_error()) { + // RpcError is a proto message; convert to C++ RpcError and throw + pr.set_exception( + std::make_exception_ptr(RpcError::fromProto(cb.error()))); + return; + } + pr.set_value(cb.payload()); + }); +} + } // namespace livekit diff --git a/src/local_participant.cpp b/src/local_participant.cpp index 2f52573..02f90d5 100644 --- a/src/local_participant.cpp +++ b/src/local_participant.cpp @@ -251,4 +251,103 @@ void LocalParticipant::unpublishTrack(const std::string &track_sid) { track_publications_.erase(track_sid); } +std::string LocalParticipant::performRpc( + const std::string &destination_identity, const std::string &method, + const std::string &payload, std::optional response_timeout) { + auto handle_id = ffiHandleId(); + if (handle_id == 0) { + throw std::runtime_error( + "LocalParticipant::performRpc: invalid FFI handle"); + } + + std::uint32_t timeout_ms = 0; + bool has_timeout = false; + if (response_timeout.has_value()) { + timeout_ms = static_cast(response_timeout.value() * 1000.0); + has_timeout = true; + } + + auto fut = FfiClient::instance().performRpcAsync( + static_cast(handle_id), destination_identity, method, + payload, + has_timeout ? std::optional(timeout_ms) : std::nullopt); + return fut.get(); +} + +void LocalParticipant::registerRpcMethod(const std::string &method_name, + RpcHandler handler) { + auto handle_id = ffiHandleId(); + if (handle_id == 0) { + throw std::runtime_error( + "LocalParticipant::registerRpcMethod: invalid FFI handle"); + } + rpc_handlers_[method_name] = std::move(handler); + FfiRequest req; + auto *msg = req.mutable_register_rpc_method(); + msg->set_local_participant_handle(static_cast(handle_id)); + msg->set_method(method_name); + + (void)FfiClient::instance().sendRequest(req); +} + +void LocalParticipant::unregisterRpcMethod(const std::string &method_name) { + auto handle_id = ffiHandleId(); + if (handle_id == 0) { + throw std::runtime_error( + "LocalParticipant::unregisterRpcMethod: invalid FFI handle"); + } + rpc_handlers_.erase(method_name); + FfiRequest req; + auto *msg = req.mutable_unregister_rpc_method(); + msg->set_local_participant_handle(static_cast(handle_id)); + msg->set_method(method_name); + + (void)FfiClient::instance().sendRequest(req); +} + +void LocalParticipant::handleRpcMethodInvocation( + uint64_t invocation_id, const std::string &method, + const std::string &request_id, const std::string &caller_identity, + const std::string &payload, double response_timeout_sec) { + std::optional response_error; + std::optional response_payload; + std::cout << "handleRpcMethodInvocation \n"; + RpcInvocationData params{request_id, caller_identity, payload, + response_timeout_sec}; + auto it = rpc_handlers_.find(method); + if (it == rpc_handlers_.end()) { + // No handler registered β†’ built-in UNSUPPORTED_METHOD + response_error = RpcError::builtIn(RpcError::ErrorCode::UNSUPPORTED_METHOD); + } else { + try { + // Invoke user handler: may return payload or throw RpcError + response_payload = it->second(params); + } catch (const RpcError &err) { + // Handler explicitly signalled an RPC error: forward as-is + response_error = err; + } catch (const std::exception &ex) { + // Any other exception: wrap as built-in APPLICATION_ERROR + response_error = + RpcError::builtIn(RpcError::ErrorCode::APPLICATION_ERROR, ex.what()); + } catch (...) { + response_error = RpcError::builtIn(RpcError::ErrorCode::APPLICATION_ERROR, + "unknown error"); + } + } + + FfiRequest req; + auto *msg = req.mutable_rpc_method_invocation_response(); + msg->set_local_participant_handle(ffiHandleId()); + msg->set_invocation_id(invocation_id); + if (response_error.has_value()) { + auto *err_proto = msg->mutable_error(); + err_proto->CopyFrom(response_error->toProto()); + } + if (response_payload.has_value()) { + msg->set_payload(*response_payload); + } + std::cout << "handleRpcMethodInvocation sendrequest \n"; + FfiClient::instance().sendRequest(req); +} + } // namespace livekit diff --git a/src/room.cpp b/src/room.cpp index f52edb1..6bf1877 100644 --- a/src/room.cpp +++ b/src/room.cpp @@ -166,8 +166,35 @@ void Room::OnEvent(const FfiEvent &event) { { std::lock_guard guard(lock_); delegate_snapshot = delegate_; - // If you want, you can also update internal state here (participants, room - // info, etc.). + } + + // First, handle RPC method invocations (not part of RoomEvent). + if (event.message_case() == FfiEvent::kRpcMethodInvocation) { + const auto &rpc = event.rpc_method_invocation(); + std::cout << "kRpcMethodInvocation \n"; + + LocalParticipant *lp = nullptr; + { + std::lock_guard guard(lock_); + if (!local_participant_) { + return; + } + auto local_handle = local_participant_->ffiHandleId(); + if (local_handle == 0 || rpc.local_participant_handle() != + static_cast(local_handle)) { + // RPC is not targeted at this room's local participant; ignore. + return; + } + lp = local_participant_.get(); + } + + // Call outside the lock to avoid deadlocks / re-entrancy issues. + lp->handleRpcMethodInvocation( + rpc.invocation_id(), rpc.method(), rpc.request_id(), + rpc.caller_identity(), rpc.payload(), + static_cast(rpc.response_timeout_ms()) / 1000.0); + + return; } if (!delegate_snapshot) { diff --git a/src/room_proto_converter.cpp b/src/room_proto_converter.cpp index 0c41a6a..7a6b1c1 100644 --- a/src/room_proto_converter.cpp +++ b/src/room_proto_converter.cpp @@ -16,6 +16,7 @@ #include "room_proto_converter.h" +#include "livekit/local_participant.h" #include "room.pb.h" namespace livekit { diff --git a/src/room_proto_converter.h b/src/room_proto_converter.h index 89ba7c4..127a70c 100644 --- a/src/room_proto_converter.h +++ b/src/room_proto_converter.h @@ -19,8 +19,12 @@ #include "livekit/room_delegate.h" #include "room.pb.h" +#include + namespace livekit { +enum class RpcErrorCode; + // --------- basic helper conversions --------- ConnectionQuality toConnectionQuality(proto::ConnectionQuality in); diff --git a/src/rpc_error.cpp b/src/rpc_error.cpp new file mode 100644 index 0000000..14c7d29 --- /dev/null +++ b/src/rpc_error.cpp @@ -0,0 +1,91 @@ +/* + * Copyright 2025 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. + */ + +#include "livekit/rpc_error.h" + +#include "rpc.pb.h" + +namespace livekit { + +RpcError::RpcError(std::uint32_t code, std::string message, std::string data) + : std::runtime_error(message), code_(code), message_(std::move(message)), + data_(std::move(data)) {} + +RpcError::RpcError(ErrorCode code, std::string message, std::string data) + : RpcError(static_cast(code), std::move(message), + std::move(data)) {} + +std::uint32_t RpcError::code() const noexcept { return code_; } + +const std::string &RpcError::message() const noexcept { return message_; } + +const std::string &RpcError::data() const noexcept { return data_; } + +proto::RpcError RpcError::toProto() const { + proto::RpcError err; + err.set_code(code_); + err.set_message(message_); + + // Set data only if non-empty; empty string means "no data". + if (!data_.empty()) { + err.set_data(data_); + } + + return err; +} + +RpcError RpcError::fromProto(const proto::RpcError &err) { + // proto::RpcError.data() will return empty string if unset, which is fine. + return RpcError(err.code(), err.message(), err.data()); +} + +RpcError RpcError::builtIn(ErrorCode code, const std::string &data) { + const char *msg = defaultMessageFor(code); + return RpcError(code, msg ? std::string(msg) : std::string{}, data); +} + +const char *RpcError::defaultMessageFor(ErrorCode code) { + // Mirror Python RpcError.ErrorMessage mapping. + switch (code) { + case ErrorCode::APPLICATION_ERROR: + return "Application error in method handler"; + case ErrorCode::CONNECTION_TIMEOUT: + return "Connection timeout"; + case ErrorCode::RESPONSE_TIMEOUT: + return "Response timeout"; + case ErrorCode::RECIPIENT_DISCONNECTED: + return "Recipient disconnected"; + case ErrorCode::RESPONSE_PAYLOAD_TOO_LARGE: + return "Response payload too large"; + case ErrorCode::SEND_FAILED: + return "Failed to send"; + case ErrorCode::UNSUPPORTED_METHOD: + return "Method not supported at destination"; + case ErrorCode::RECIPIENT_NOT_FOUND: + return "Recipient not found"; + case ErrorCode::REQUEST_PAYLOAD_TOO_LARGE: + return "Request payload too large"; + case ErrorCode::UNSUPPORTED_SERVER: + return "RPC not supported by server"; + case ErrorCode::UNSUPPORTED_VERSION: + return "Unsupported RPC version"; + } + + // Should be unreachable if all enum values are covered. + return ""; +} + +} // namespace livekit \ No newline at end of file From fd772ffbbbaa7c614151d43884bac956d1f902b8 Mon Sep 17 00:00:00 2001 From: shijing xian Date: Fri, 5 Dec 2025 12:16:15 -0800 Subject: [PATCH 02/12] added a new README and fix the comments --- README.md | 2 +- examples/simple_rpc/README | 157 +++++++++++++++++++++++++++++++++++ examples/simple_rpc/main.cpp | 41 ++------- 3 files changed, 163 insertions(+), 37 deletions(-) create mode 100644 examples/simple_rpc/README diff --git a/README.md b/README.md index cc55a3a..fe44301 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,7 @@ The SimpleRpc example demonstrates how to: - Observe round-trip times (RTT) for each RPC call #### πŸ”‘ Generate Tokens -Before running any participant, create JWT tokens with "caller", "greeter" and "math-genius" identities and room name. +Before running any participant, create JWT tokens with **caller**, **greeter** and **math-genius** identities and room name. ```bash lk token create -r test -i caller --join --valid-for 99999h --dev --room=your_own_room lk token create -r test -i greeter --join --valid-for 99999h --dev --room=your_own_room diff --git a/examples/simple_rpc/README b/examples/simple_rpc/README new file mode 100644 index 0000000..64bc870 --- /dev/null +++ b/examples/simple_rpc/README @@ -0,0 +1,157 @@ +# πŸ“˜ SimpleRpc Example β€” Technical Overview + +This README provides deeper technical details about the RPC (Remote Procedure Call) support demonstrated in the SimpleRpc example. +It complements the example instructions found in the root README.md. + +If you're looking for how to run the example, see the root [README](https://github.com/livekit/client-sdk-cpp). + +This document explains: +- How LiveKit RPC works in the C++ SDK +- Where the APIs are defined +- How senders call RPC methods +- How receivers register handlers +- What happens if the receiver is absent +- How long-running operations behave +- Timeouts, disconnects, and unsupported methods +- RPC lifecycle events and error propagation + +## πŸ”§ Overview: How RPC Works +LiveKit RPC allows one participant (the caller) to invoke a method on another participant (the receiver) using the data channel transport. +It is: +- Peer-to-peer within the room (not server-executed RPC) +- Request/response (caller waits for a reply or an error) +- Asynchronous under the hood, synchronous or blocking from the caller’s perspective +- Delivery-guaranteed when using the reliable data channel + +Each RPC call includes: +| Field | Meaning | +|--------------------------|-----------------------------------------------------| +| **destination_identity** | Identity of the target participant | +| **method** | Method name string (e.g., "square-root") | +| **payload** | Arbitrary UTF-8 text | +| **response_timeout** | Optional timeout (seconds) | +| **invocation_id** | Server-generated ID used internally for correlation | + +## πŸ“ Location of APIs in C++ +All public-facing RPC APIs live in: +[include/livekit/local_participant.h](https://github.com/livekit/client-sdk-cpp/blob/main/include/livekit/local_participant.h#L124) + +### Key methods: + +#### Sender-side APIs +```bash +std::string performRpc( + const std::string& destination_identity, + const std::string& method, + const std::string& payload, + std::optional response_timeout_sec = std::nullopt +); + +Receiver-side APIs +void registerRpcMethod( + const std::string& method_name, + RpcHandler handler +); + +void unregisterRpcMethod(const std::string& method_name); + +Handler signature +using RpcHandler = + std::function(const RpcInvocationData&)>; +``` + +Handlers can: +- Return a string (the RPC response payload) +- Return std::nullopt (meaning β€œno return payload”) +- Throw exceptions (mapped to APPLICATION_ERROR) +- Throw a RpcError (mapped to specific error codes) + +#### πŸ›° Sender Behavior (performRpc) + +When the caller invokes: +```bash +auto reply = lp->performRpc("math-genius", "square-root", "{\"number\":16}"); +``` + +The following occurs: + +A PerformRpcRequest is sent through FFI to the SDK core. + +The SDK transmits the invocation to the target participant (if present). + +The caller begins waiting for a matching RpcMethodInvocationResponse. + +One of the following happens: +| Outcome | Meaning | +|--------------------------|------------------------------------------| +| **Success** | Receiver returned a payload | +| **UNSUPPORTED_METHOD** | Receiver did not register the method | +| **RECIPIENT_NOT_FOUND** | Target identity not present in room | +| **RECIPIENT_DISCONNECTED** | Target left before replying | +| **RESPONSE_TIMEOUT** | Receiver took too long | +| **APPLICATION_ERROR** | Handler threw an exception | + +#### πŸ”„ Round-trip time (RTT) + +The caller can measure RTT externally (as SimpleRpc does), but the SDK does not measure RTT internally. + +#### πŸ“‘ Receiver Behavior (registerRpcMethod) + +A receiver must explicitly register handlers: +```bash +local_participant->registerRpcMethod("square-root", + [](const RpcInvocationData& data) { + double number = parse(data.payload); + return make_json("result", std::sqrt(number)); + }); +``` + +When an invocation arrives: +- Room receives a RpcMethodInvocationEvent +- Room forwards it to the corresponding LocalParticipant +- LocalParticipant::handleRpcMethodInvocation(): +- Calls the handler +- Converts any exceptions into RpcError +- Sends back RpcMethodInvocationResponse + +⚠ If no handler exists: + +Receiver returns: UNSUPPORTED_METHOD + + +#### 🚨 What Happens if Receiver Is Absent? +| Case | Behavior | +|-----------------------------------------------------|---------------------------------------------------| +| Receiver identity is not in the room | Caller immediately receives `RECIPIENT_NOT_FOUND` | +| Receiver is present but disconnects before replying | Caller receives `RECIPIENT_DISCONNECTED` | +| Receiver joins later | Caller must retry manually (no automatic waiting) | + +**Important**: +LiveKit does not queue RPC calls for offline participants. + +#### ⏳ Timeout Behavior + +If the caller specifies: + +performRpc(..., /*response_timeout=*/10.0); + +Then: +- Receiver is given 10 seconds to respond. +- If the receiver handler takes longer (e.g., sleep 30s), caller receives: +RESPONSE_TIMEOUT + +**If no response_timeout is provided explicitly, the default timeout is 15 seconds.** + + +This is by design and demonstrated in the example. + +#### 🧨 Errors & Failure Modes +| Error Code | Cause | +|------------------------|---------------------------------------------| +| **APPLICATION_ERROR** | Handler threw a C++ exception | +| **UNSUPPORTED_METHOD** | No handler registered for the method | +| **RECIPIENT_NOT_FOUND** | Destination identity not in room | +| **RECIPIENT_DISCONNECTED** | Participant left mid-flight | +| **RESPONSE_TIMEOUT** | Handler exceeded allowed response time | +| **CONNECTION_TIMEOUT** | Transport-level issue | +| **SEND_FAILED** | SDK failed to send invocation | diff --git a/examples/simple_rpc/main.cpp b/examples/simple_rpc/main.cpp index 4686bb5..ff5c4af 100644 --- a/examples/simple_rpc/main.cpp +++ b/examples/simple_rpc/main.cpp @@ -32,17 +32,13 @@ #include #include "livekit/livekit.h" -#include "livekit_ffi.h" // same as simple_room; internal but used here +#include "livekit_ffi.h" using namespace livekit; using namespace std::chrono_literals; namespace { -// ------------------------------------------------------------ -// Global control (same pattern as simple_room) -// ------------------------------------------------------------ - std::atomic g_running{true}; void handleSignal(int) { g_running.store(false); } @@ -91,38 +87,31 @@ bool ensurePeerPresent(Room &room, const std::string &identity, std::cout << "[Caller] Waiting up to " << timeout.count() << "s for " << friendly_role << " (identity=\"" << identity << "\") to join...\n"; - bool present = waitForParticipant( room, identity, std::chrono::duration_cast(timeout)); - if (present) { std::cout << "[Caller] " << friendly_role << " is present.\n"; return true; } - // Timed out auto info = room.room_info(); const std::string room_name = info.name; - std::cout << "[Caller] Timed out after " << timeout.count() << "s waiting for " << friendly_role << " (identity=\"" << identity << "\").\n"; std::cout << "[Caller] No participant with identity \"" << identity << "\" appears to be connected to room \"" << room_name << "\".\n\n"; - std::cout << "To start a " << friendly_role << " in another terminal, run:\n\n" << " lk token create -r test -i " << identity << " --join --valid-for 99999h --dev --room=" << room_name << "\n" << " ./build/examples/SimpleRpc " << url << " $token --role=" << friendly_role << "\n\n"; - return false; } -// Parse args similar to simple_room, plus optional --role / role positional bool parseArgs(int argc, char *argv[], std::string &url, std::string &token, std::string &role) { // --help @@ -206,26 +195,18 @@ bool parseArgs(int argc, char *argv[], std::string &url, std::string &token, return !(url.empty() || token.empty()); } -// ------------------------------------------------------------ -// Tiny helpers for the simple JSON used in the sample -// (to avoid bringing in a json library) -// ------------------------------------------------------------ - -// create {"key":number} std::string makeNumberJson(const std::string &key, double value) { std::ostringstream oss; oss << "{\"" << key << "\":" << value << "}"; return oss.str(); } -// create {"key":"value"} std::string makeStringJson(const std::string &key, const std::string &value) { std::ostringstream oss; oss << "{\"" << key << "\":\"" << value << "\"}"; return oss.str(); } -// very naive parse of {"key":number} double parseNumberFromJson(const std::string &json) { auto colon = json.find(':'); if (colon == std::string::npos) @@ -236,7 +217,6 @@ double parseNumberFromJson(const std::string &json) { return std::stod(num_str); } -// very naive parse of {"key":"value"} std::string parseStringFromJson(const std::string &json) { auto colon = json.find(':'); if (colon == std::string::npos) @@ -250,10 +230,7 @@ std::string parseStringFromJson(const std::string &json) { return json.substr(first_quote + 1, second_quote - first_quote - 1); } -// ------------------------------------------------------------ -// RPC handler registration (for greeter & math-genius) -// ------------------------------------------------------------ - +// RPC handler registration void registerReceiverMethods(Room &greeters_room, Room &math_genius_room) { LocalParticipant *greeter_lp = greeters_room.local_participant(); LocalParticipant *math_genius_lp = math_genius_room.local_participant(); @@ -321,19 +298,15 @@ void registerReceiverMethods(Room &greeters_room, Room &math_genius_room) { std::cout << "[Math Genius] This will take 30 seconds even though " "you're only giving me " << data.response_timeout_sec << " seconds\n"; - + // Sleep for 30 seconds to mimic a long running task. std::this_thread::sleep_for(30s); return makeStringJson("result", "Calculation complete!"); }); // Note: we do NOT register "quantum-hypergeometric-series" here, - // so the caller sees UNSUPPORTED_METHOD, just like in Python. + // so the caller sees UNSUPPORTED_METHOD } -// ------------------------------------------------------------ -// Caller-side helpers (like perform_* in rpc.py) -// ------------------------------------------------------------ - void performGreeting(Room &room) { std::cout << "[Caller] Letting the greeter know that I've arrived\n"; double t0 = nowMs(); @@ -460,10 +433,6 @@ void performLongCalculation(Room &room) { } // namespace -// ------------------------------------------------------------ -// main – similar style to simple_room/main.cpp -// ------------------------------------------------------------ - int main(int argc, char *argv[]) { std::string url, token, role; if (!parseArgs(argc, argv, url, token, role)) { @@ -479,7 +448,7 @@ int main(int argc, char *argv[]) { std::cout << "Connecting to: " << url << "\n"; std::cout << "Role: " << role << "\n"; - // Ctrl-C + // Ctrl-C to quit the program std::signal(SIGINT, handleSignal); Room room{}; From 615921445ded8af4d6eb63bdfa9efbe8f7aa35c9 Mon Sep 17 00:00:00 2001 From: shijing xian Date: Fri, 5 Dec 2025 12:17:21 -0800 Subject: [PATCH 03/12] point to the right line --- examples/simple_rpc/README | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/simple_rpc/README b/examples/simple_rpc/README index 64bc870..2ded78c 100644 --- a/examples/simple_rpc/README +++ b/examples/simple_rpc/README @@ -34,7 +34,7 @@ Each RPC call includes: ## πŸ“ Location of APIs in C++ All public-facing RPC APIs live in: -[include/livekit/local_participant.h](https://github.com/livekit/client-sdk-cpp/blob/main/include/livekit/local_participant.h#L124) +[include/livekit/local_participant.h](https://github.com/livekit/client-sdk-cpp/blob/main/include/livekit/local_participant.h#L160) ### Key methods: From 94f00cef2a393c28e3ed98ad1b2322351530b42f Mon Sep 17 00:00:00 2001 From: shijing xian Date: Fri, 5 Dec 2025 16:23:12 -0800 Subject: [PATCH 04/12] fix the linux build --- include/livekit/local_participant.h | 1 + 1 file changed, 1 insertion(+) diff --git a/include/livekit/local_participant.h b/include/livekit/local_participant.h index 28aa70a..17dc9a0 100644 --- a/include/livekit/local_participant.h +++ b/include/livekit/local_participant.h @@ -22,6 +22,7 @@ #include "livekit/rpc_error.h" #include +#include #include #include #include From b29e8bcef234cddb5e8a2bff97dd564dfb868d54 Mon Sep 17 00:00:00 2001 From: shijing xian Date: Mon, 8 Dec 2025 20:31:24 -0800 Subject: [PATCH 05/12] fix the build and addressed the comments --- examples/simple_rpc/main.cpp | 1 + src/room.cpp | 596 ++++++++++++++++++++++++++++++----- src/room_event_converter.h | 107 ------- 3 files changed, 525 insertions(+), 179 deletions(-) delete mode 100644 src/room_event_converter.h diff --git a/examples/simple_rpc/main.cpp b/examples/simple_rpc/main.cpp index ff5c4af..61151db 100644 --- a/examples/simple_rpc/main.cpp +++ b/examples/simple_rpc/main.cpp @@ -16,6 +16,7 @@ #include #include +#include #include #include #include diff --git a/src/room.cpp b/src/room.cpp index 6bf1877..4f29583 100644 --- a/src/room.cpp +++ b/src/room.cpp @@ -45,7 +45,7 @@ using proto::FfiResponse; namespace { -std::unique_ptr +std::shared_ptr createRemoteParticipant(const proto::OwnedParticipant &owned) { const auto &pinfo = owned.info(); std::unordered_map attrs; @@ -56,7 +56,7 @@ createRemoteParticipant(const proto::OwnedParticipant &owned) { auto kind = livekit::fromProto(pinfo.kind()); auto reason = livekit::toDisconnectReason(pinfo.disconnect_reason()); livekit::FfiHandle handle(static_cast(owned.handle().id())); - return std::make_unique( + return std::make_shared( std::move(handle), pinfo.sid(), pinfo.name(), pinfo.identity(), pinfo.metadata(), std::move(attrs), kind, reason); } @@ -125,8 +125,8 @@ bool Room::Connect(const std::string &url, const std::string &token, for (const auto &owned_publication_info : pt.publications()) { auto publication = std::make_shared(owned_publication_info); - rp->mutable_track_publications().emplace(publication->sid(), - std::move(publication)); + rp->mutableTrackPublications().emplace(publication->sid(), + std::move(publication)); } remote_participants_.emplace(rp->identity(), std::move(rp)); @@ -171,7 +171,6 @@ void Room::OnEvent(const FfiEvent &event) { // First, handle RPC method invocations (not part of RoomEvent). if (event.message_case() == FfiEvent::kRpcMethodInvocation) { const auto &rpc = event.rpc_method_invocation(); - std::cout << "kRpcMethodInvocation \n"; LocalParticipant *lp = nullptr; { @@ -180,8 +179,9 @@ void Room::OnEvent(const FfiEvent &event) { return; } auto local_handle = local_participant_->ffiHandleId(); - if (local_handle == 0 || rpc.local_participant_handle() != - static_cast(local_handle)) { + if (local_handle == INVALID_HANDLE || + rpc.local_participant_handle() != + static_cast(local_handle)) { // RPC is not targeted at this room's local participant; ignore. return; } @@ -210,27 +210,34 @@ void Room::OnEvent(const FfiEvent &event) { switch (re.message_case()) { case proto::RoomEvent::kParticipantConnected: { - auto ev = fromProto(re.participant_connected()); - std::cout << "kParticipantConnected " << std::endl; - // Create and register RemoteParticipant + std::shared_ptr new_participant; { std::lock_guard guard(lock_); - auto rp = createRemoteParticipant(re.participant_connected().info()); - remote_participants_.emplace(rp->identity(), std::move(rp)); + const auto &owned = re.participant_connected().info(); + // createRemoteParticipant takes proto::OwnedParticipant + new_participant = createRemoteParticipant(owned); + remote_participants_.emplace(new_participant->identity(), + new_participant); } - // TODO, use better public callback events + ParticipantConnectedEvent ev; + ev.participant = new_participant.get(); delegate_snapshot->onParticipantConnected(*this, ev); break; } case proto::RoomEvent::kParticipantDisconnected: { - auto ev = fromProto(re.participant_disconnected()); + std::shared_ptr removed; + DisconnectReason reason = DisconnectReason::Unknown; + { std::lock_guard guard(lock_); const auto &pd = re.participant_disconnected(); const std::string &identity = pd.participant_identity(); + reason = toDisconnectReason(pd.disconnect_reason()); + auto it = remote_participants_.find(identity); if (it != remote_participants_.end()) { + removed = it->second; remote_participants_.erase(it); } else { // We saw a disconnect event for a participant we don't track @@ -240,28 +247,86 @@ void Room::OnEvent(const FfiEvent &event) { << identity << std::endl; } } - // TODO, should we trigger onParticipantDisconnected if remote - // participants can't be found ? - delegate_snapshot->onParticipantDisconnected(*this, ev); + if (removed) { + ParticipantDisconnectedEvent ev; + ev.participant = removed.get(); + ev.reason = reason; + delegate_snapshot->onParticipantDisconnected(*this, ev); + } break; } case proto::RoomEvent::kLocalTrackPublished: { - auto ev = fromProto(re.local_track_published()); + LocalTrackPublishedEvent ev; + { + std::lock_guard guard(lock_); + if (!local_participant_) { + std::cerr << "kLocalTrackPublished: local_participant_ is nullptr" + << std::endl; + break; + } + const auto <p = re.local_track_published(); + const std::string &sid = ltp.track_sid(); + auto &pubs = local_participant_->trackPublications(); + auto it = pubs.find(sid); + if (it == pubs.end()) { + std::cerr << "local_track_published for unknown sid: " << sid + << std::endl; + break; + } + ev.publication = it->second; + ev.track = ev.publication ? ev.publication->track() : nullptr; + } delegate_snapshot->onLocalTrackPublished(*this, ev); break; } case proto::RoomEvent::kLocalTrackUnpublished: { - auto ev = fromProto(re.local_track_unpublished()); + LocalTrackUnpublishedEvent ev; + { + std::lock_guard guard(lock_); + if (!local_participant_) { + std::cerr << "kLocalTrackPublished: local_participant_ is nullptr" + << std::endl; + break; + } + const auto <u = re.local_track_unpublished(); + const std::string &pub_sid = ltu.publication_sid(); + auto &pubs = local_participant_->trackPublications(); + auto it = pubs.find(pub_sid); + if (it == pubs.end()) { + std::cerr << "local_track_unpublished for unknown publication sid: " + << pub_sid << std::endl; + break; + } + ev.publication = it->second; + } delegate_snapshot->onLocalTrackUnpublished(*this, ev); break; } case proto::RoomEvent::kLocalTrackSubscribed: { - auto ev = fromProto(re.local_track_subscribed()); + LocalTrackSubscribedEvent ev; + { + std::lock_guard guard(lock_); + if (!local_participant_) { + break; + } + const auto <s = re.local_track_subscribed(); + const std::string &sid = lts.track_sid(); + auto &pubs = local_participant_->trackPublications(); + auto it = pubs.find(sid); + if (it == pubs.end()) { + std::cerr << "local_track_subscribed for unknown sid: " << sid + << std::endl; + break; + } + auto publication = it->second; + ev.track = publication ? publication->track() : nullptr; + } + delegate_snapshot->onLocalTrackSubscribed(*this, ev); break; } case proto::RoomEvent::kTrackPublished: { - auto ev = fromProto(re.track_published()); + TrackPublishedEvent ev; { std::lock_guard guard(lock_); const auto &tp = re.track_published(); @@ -273,13 +338,14 @@ void Room::OnEvent(const FfiEvent &event) { auto rpublication = std::make_shared(owned_publication); // Store it on the participant, keyed by SID - rparticipant->mutable_track_publications().emplace( + rparticipant->mutableTrackPublications().emplace( rpublication->sid(), std::move(rpublication)); - + ev.participant = rparticipant; + ev.publication = rpublication; } else { // Optional: log if we get a track for an unknown participant std::cerr << "track_published for unknown participant: " << identity - << "\n"; + << std::endl; // Don't emit the break; } @@ -288,7 +354,31 @@ void Room::OnEvent(const FfiEvent &event) { break; } case proto::RoomEvent::kTrackUnpublished: { - auto ev = fromProto(re.track_unpublished()); + TrackUnpublishedEvent ev; + { + std::lock_guard guard(lock_); + const auto &tu = re.track_unpublished(); + const std::string &identity = tu.participant_identity(); + const std::string &pub_sid = tu.publication_sid(); + auto pit = remote_participants_.find(identity); + if (pit == remote_participants_.end()) { + std::cerr << "track_unpublished for unknown participant: " << identity + << std::endl; + break; + } + RemoteParticipant *rparticipant = pit->second.get(); + auto &pubs = rparticipant->mutableTrackPublications(); + auto it = pubs.find(pub_sid); + if (it == pubs.end()) { + std::cerr << "track_unpublished for unknown publication sid " + << pub_sid << " (participant " << identity << ")\n"; + break; + } + ev.participant = rparticipant; + ev.publication = it->second; + pubs.erase(it); + } + delegate_snapshot->onTrackUnpublished(*this, ev); break; } @@ -311,7 +401,7 @@ void Room::OnEvent(const FfiEvent &event) { } rparticipant = pit->second.get(); // Find existing publication by track SID (from track_published) - auto &pubs = rparticipant->mutable_track_publications(); + auto &pubs = rparticipant->mutableTrackPublications(); auto pubIt = pubs.find(track_info.sid()); if (pubIt == pubs.end()) { std::cerr << "track_subscribed for unknown publication sid " @@ -331,13 +421,9 @@ void Room::OnEvent(const FfiEvent &event) { << track_info.kind() << "\n"; break; } - std::cout << "before setTrack " << std::endl; - // Attach to publication, mark subscribed rpublication->setTrack(remote_track); - std::cout << "setTrack " << std::endl; rpublication->setSubscribed(true); - std::cout << "setSubscribed " << std::endl; } // Emit remote track_subscribed-style callback @@ -345,111 +431,478 @@ void Room::OnEvent(const FfiEvent &event) { ev.track = remote_track; ev.publication = rpublication; ev.participant = rparticipant; - std::cout << "onTrackSubscribed " << std::endl; delegate_snapshot->onTrackSubscribed(*this, ev); - std::cout << "after onTrackSubscribed " << std::endl; break; } case proto::RoomEvent::kTrackUnsubscribed: { - auto ev = fromProto(re.track_unsubscribed()); + TrackUnsubscribedEvent ev; + { + std::lock_guard guard(lock_); + const auto &tu = re.track_unsubscribed(); + const std::string &identity = tu.participant_identity(); + const std::string &track_sid = tu.track_sid(); + auto pit = remote_participants_.find(identity); + if (pit == remote_participants_.end()) { + std::cerr << "track_unsubscribed for unknown participant: " + << identity << "\n"; + break; + } + RemoteParticipant *rparticipant = pit->second.get(); + auto &pubs = rparticipant->mutableTrackPublications(); + auto pubIt = pubs.find(track_sid); + if (pubIt == pubs.end()) { + std::cerr << "track_unsubscribed for unknown publication sid " + << track_sid << " (participant " << identity << ")\n"; + break; + } + auto publication = pubIt->second; + auto track = publication->track(); + publication->setTrack(nullptr); + publication->setSubscribed(false); + ev.participant = rparticipant; + ev.publication = publication; + ev.track = track; + } + delegate_snapshot->onTrackUnsubscribed(*this, ev); break; } case proto::RoomEvent::kTrackSubscriptionFailed: { - auto ev = fromProto(re.track_subscription_failed()); + TrackSubscriptionFailedEvent ev; + { + std::lock_guard guard(lock_); + const auto &tsf = re.track_subscription_failed(); + const std::string &identity = tsf.participant_identity(); + auto pit = remote_participants_.find(identity); + if (pit == remote_participants_.end()) { + std::cerr << "track_subscription_failed for unknown participant: " + << identity << "\n"; + break; + } + ev.participant = pit->second.get(); + ev.track_sid = tsf.track_sid(); + ev.error = tsf.error(); + } delegate_snapshot->onTrackSubscriptionFailed(*this, ev); break; } case proto::RoomEvent::kTrackMuted: { - auto ev = fromProto(re.track_muted()); - delegate_snapshot->onTrackMuted(*this, ev); + TrackMutedEvent ev; + bool success = false; + { + std::lock_guard guard(lock_); + const auto &tm = re.track_muted(); + const std::string &identity = tm.participant_identity(); + const std::string &sid = tm.track_sid(); + Participant *participant = nullptr; + if (local_participant_ && local_participant_->identity() == identity) { + participant = local_participant_.get(); + } else { + auto pit = remote_participants_.find(identity); + if (pit != remote_participants_.end()) { + participant = pit->second.get(); + } + } + if (!participant) { + std::cerr << "track_muted for unknown participant: " << identity + << "\n"; + break; + } + auto pub = participant->findTrackPublication(sid); + if (!pub) { + std::cerr << "track_muted for unknown track sid: " << sid + << std::endl; + } else { + pub->setMuted(true); + if (auto t = pub->track()) { + t->setMuted(true); + } + ev.participant = participant; + ev.publication = pub; + success = true; + } + } + if (success) { + delegate_snapshot->onTrackMuted(*this, ev); + } break; } case proto::RoomEvent::kTrackUnmuted: { - auto ev = fromProto(re.track_unmuted()); - delegate_snapshot->onTrackUnmuted(*this, ev); + TrackUnmutedEvent ev; + bool success = false; + { + std::lock_guard guard(lock_); + const auto &tu = re.track_unmuted(); + const std::string &identity = tu.participant_identity(); + const std::string &sid = tu.track_sid(); + Participant *participant = nullptr; + if (local_participant_ && local_participant_->identity() == identity) { + participant = local_participant_.get(); + } else { + auto pit = remote_participants_.find(identity); + if (pit != remote_participants_.end()) { + participant = pit->second.get(); + } + } + if (!participant) { + std::cerr << "track_unmuted for unknown participant: " << identity + << "\n"; + break; + } + + auto pub = participant->findTrackPublication(sid); + if (!pub) { + std::cerr << "track_muted for unknown track sid: " << sid + << std::endl; + } else { + pub->setMuted(false); + if (auto t = pub->track()) { + t->setMuted(false); + } + ev.participant = participant; + ev.publication = pub; + success = true; + } + + ev.participant = participant; + ev.publication = pub; + } + + if (success) { + delegate_snapshot->onTrackUnmuted(*this, ev); + } break; } case proto::RoomEvent::kActiveSpeakersChanged: { - auto ev = fromProto(re.active_speakers_changed()); + ActiveSpeakersChangedEvent ev; + { + std::lock_guard guard(lock_); + const auto &asc = re.active_speakers_changed(); + for (const auto &identity : asc.participant_identities()) { + Participant *participant = nullptr; + if (local_participant_ && + local_participant_->identity() == identity) { + participant = local_participant_.get(); + } else { + auto pit = remote_participants_.find(identity); + if (pit != remote_participants_.end()) { + participant = pit->second.get(); + } + } + if (participant) { + ev.speakers.push_back(participant); + } + } + } delegate_snapshot->onActiveSpeakersChanged(*this, ev); break; } case proto::RoomEvent::kRoomMetadataChanged: { - auto ev = fromProto(re.room_metadata_changed()); + RoomMetadataChangedEvent ev; + { + std::lock_guard guard(lock_); + const auto old_metadata = room_info_.metadata; + room_info_.metadata = re.room_metadata_changed().metadata(); + ev.old_metadata = old_metadata; + ev.new_metadata = room_info_.metadata; + } delegate_snapshot->onRoomMetadataChanged(*this, ev); break; } case proto::RoomEvent::kRoomSidChanged: { - auto ev = fromProto(re.room_sid_changed()); + RoomSidChangedEvent ev; + { + std::lock_guard guard(lock_); + room_info_.sid = re.room_sid_changed().sid(); + ev.sid = room_info_.sid.value_or(std::string{}); + } delegate_snapshot->onRoomSidChanged(*this, ev); break; } case proto::RoomEvent::kParticipantMetadataChanged: { - auto ev = fromProto(re.participant_metadata_changed()); + ParticipantMetadataChangedEvent ev; + { + std::lock_guard guard(lock_); + const auto &pm = re.participant_metadata_changed(); + const std::string &identity = pm.participant_identity(); + Participant *participant = nullptr; + if (local_participant_ && local_participant_->identity() == identity) { + participant = local_participant_.get(); + } else { + auto it = remote_participants_.find(identity); + if (it != remote_participants_.end()) { + participant = it->second.get(); + } + } + if (!participant) { + std::cerr << "participant_metadata_changed for unknown participant: " + << identity << "\n"; + break; + } + std::string old_metadata = participant->metadata(); + participant->set_metadata(pm.metadata()); + ev.participant = participant; + ev.old_metadata = old_metadata; + ev.new_metadata = participant->metadata(); + } + delegate_snapshot->onParticipantMetadataChanged(*this, ev); break; } case proto::RoomEvent::kParticipantNameChanged: { - auto ev = fromProto(re.participant_name_changed()); + ParticipantNameChangedEvent ev; + { + std::lock_guard guard(lock_); + const auto &pn = re.participant_name_changed(); + const std::string &identity = pn.participant_identity(); + Participant *participant = nullptr; + if (local_participant_ && local_participant_->identity() == identity) { + participant = local_participant_.get(); + } else { + auto it = remote_participants_.find(identity); + if (it != remote_participants_.end()) { + participant = it->second.get(); + } + } + if (!participant) { + std::cerr << "participant_name_changed for unknown participant: " + << identity << "\n"; + break; + } + std::string old_name = participant->name(); + participant->set_name(pn.name()); + ev.participant = participant; + ev.old_name = old_name; + ev.new_name = participant->name(); + } delegate_snapshot->onParticipantNameChanged(*this, ev); break; } case proto::RoomEvent::kParticipantAttributesChanged: { - auto ev = fromProto(re.participant_attributes_changed()); + ParticipantAttributesChangedEvent ev; + { + std::lock_guard guard(lock_); + const auto &pa = re.participant_attributes_changed(); + const std::string &identity = pa.participant_identity(); + Participant *participant = nullptr; + if (local_participant_ && local_participant_->identity() == identity) { + participant = local_participant_.get(); + } else { + auto it = remote_participants_.find(identity); + if (it != remote_participants_.end()) { + participant = it->second.get(); + } + } + if (!participant) { + std::cerr + << "participant_attributes_changed for unknown participant: " + << identity << "\n"; + break; + } + // Build full attributes map + std::unordered_map attrs; + for (const auto &entry : pa.attributes()) { + attrs.emplace(entry.key(), entry.value()); + } + participant->set_attributes(attrs); + + // Build changed_attributes map + for (const auto &entry : pa.changed_attributes()) { + ev.changed_attributes.emplace_back(entry.key(), entry.value()); + } + ev.participant = participant; + } delegate_snapshot->onParticipantAttributesChanged(*this, ev); break; } case proto::RoomEvent::kParticipantEncryptionStatusChanged: { - auto ev = fromProto(re.participant_encryption_status_changed()); + ParticipantEncryptionStatusChangedEvent ev; + { + std::lock_guard guard(lock_); + const auto &pe = re.participant_encryption_status_changed(); + const std::string &identity = pe.participant_identity(); + Participant *participant = nullptr; + if (local_participant_ && local_participant_->identity() == identity) { + participant = local_participant_.get(); + } else { + auto it = remote_participants_.find(identity); + if (it != remote_participants_.end()) { + participant = it->second.get(); + } + } + if (!participant) { + std::cerr << "participant_encryption_status_changed for unknown " + "participant: " + << identity << "\n"; + break; + } + ev.participant = participant; + ev.is_encrypted = pe.is_encrypted(); + } + delegate_snapshot->onParticipantEncryptionStatusChanged(*this, ev); break; } case proto::RoomEvent::kConnectionQualityChanged: { - auto ev = fromProto(re.connection_quality_changed()); + ConnectionQualityChangedEvent ev; + { + std::lock_guard guard(lock_); + const auto &cq = re.connection_quality_changed(); + const std::string &identity = cq.participant_identity(); + Participant *participant = nullptr; + if (local_participant_ && local_participant_->identity() == identity) { + participant = local_participant_.get(); + } else { + auto it = remote_participants_.find(identity); + if (it != remote_participants_.end()) { + participant = it->second.get(); + } + } + if (!participant) { + std::cerr << "connection_quality_changed for unknown participant: " + << identity << "\n"; + break; + } + ev.participant = participant; + ev.quality = static_cast(cq.quality()); + } + delegate_snapshot->onConnectionQualityChanged(*this, ev); break; } + + // ------------------------------------------------------------------------ + // Transcription + // ------------------------------------------------------------------------ + + case proto::RoomEvent::kTranscriptionReceived: { + TranscriptionReceivedEvent ev; + { + std::lock_guard guard(lock_); + const auto &tr = re.transcription_received(); + for (const auto &s : tr.segments()) { + TranscriptionSegment seg; + seg.id = s.id(); + seg.text = s.text(); + seg.final = s.final(); + seg.start_time = s.start_time(); + seg.end_time = s.end_time(); + seg.language = s.language(); + ev.segments.push_back(std::move(seg)); + } + + Participant *participant = nullptr; + if (!tr.participant_identity().empty()) { + const std::string &identity = tr.participant_identity(); + if (local_participant_ && + local_participant_->identity() == identity) { + participant = local_participant_.get(); + } else { + auto it = remote_participants_.find(identity); + if (it != remote_participants_.end()) { + participant = it->second.get(); + } + } + } + ev.participant = participant; + ev.publication = participant->findTrackPublication(tr.track_sid()); + } + + delegate_snapshot->onTranscriptionReceived(*this, ev); + break; + } + + // ------------------------------------------------------------------------ + // Data packets: user vs SIP DTMF + // ------------------------------------------------------------------------ + case proto::RoomEvent::kDataPacketReceived: { + const auto &dp = re.data_packet_received(); + RemoteParticipant *rp = nullptr; + { + std::lock_guard guard(lock_); + auto it = remote_participants_.find(dp.participant_identity()); + if (it != remote_participants_.end()) { + rp = it->second.get(); + } + } + const auto which_val = dp.value_case(); + if (which_val == proto::DataPacketReceived::kUser) { + UserDataPacketEvent ev = userDataPacketFromProto(dp, rp); + delegate_snapshot->onUserPacketReceived(*this, ev); + } else if (which_val == proto::DataPacketReceived::kSipDtmf) { + SipDtmfReceivedEvent ev = sipDtmfFromProto(dp, rp); + delegate_snapshot->onSipDtmfReceived(*this, ev); + } + break; + } + + // ------------------------------------------------------------------------ + // E2EE state + // ------------------------------------------------------------------------ + case proto::RoomEvent::kE2EeStateChanged: { + E2eeStateChangedEvent ev; + { + std::lock_guard guard(lock_); + const auto &es = re.e2ee_state_changed(); + const std::string &identity = es.participant_identity(); + Participant *participant = nullptr; + if (local_participant_ && local_participant_->identity() == identity) { + participant = local_participant_.get(); + } else { + auto it = remote_participants_.find(identity); + if (it != remote_participants_.end()) { + participant = it->second.get(); + } + } + if (!participant) { + std::cerr << "e2ee_state_changed for unknown participant: " + << identity << std::endl; + break; + } + + ev.participant = participant; + ev.state = static_cast(es.state()); + } + delegate_snapshot->onE2eeStateChanged(*this, ev); + break; + } + + // ------------------------------------------------------------------------ + // Connection state / lifecycle + // ------------------------------------------------------------------------ + case proto::RoomEvent::kConnectionStateChanged: { - auto ev = fromProto(re.connection_state_changed()); + ConnectionStateChangedEvent ev; + { + std::lock_guard guard(lock_); + const auto &cs = re.connection_state_changed(); + connection_state_ = static_cast(cs.state()); + ev.state = connection_state_; + } delegate_snapshot->onConnectionStateChanged(*this, ev); break; } case proto::RoomEvent::kDisconnected: { - auto ev = fromProto(re.disconnected()); + DisconnectedEvent ev; + ev.reason = toDisconnectReason(re.disconnected().reason()); delegate_snapshot->onDisconnected(*this, ev); break; } case proto::RoomEvent::kReconnecting: { - auto ev = fromProto(re.reconnecting()); + ReconnectingEvent ev; delegate_snapshot->onReconnecting(*this, ev); break; } case proto::RoomEvent::kReconnected: { - auto ev = fromProto(re.reconnected()); + ReconnectedEvent ev; delegate_snapshot->onReconnected(*this, ev); break; } - case proto::RoomEvent::kE2EeStateChanged: { - auto ev = fromProto(re.e2ee_state_changed()); - delegate_snapshot->onE2eeStateChanged(*this, ev); - break; - } case proto::RoomEvent::kEos: { - auto ev = fromProto(re.eos()); + RoomEosEvent ev; delegate_snapshot->onRoomEos(*this, ev); break; } - case proto::RoomEvent::kDataPacketReceived: { - auto ev = fromProto(re.data_packet_received()); - delegate_snapshot->onDataPacketReceived(*this, ev); - break; - } - case proto::RoomEvent::kTranscriptionReceived: { - auto ev = fromProto(re.transcription_received()); - delegate_snapshot->onTranscriptionReceived(*this, ev); - break; - } case proto::RoomEvent::kChatMessage: { auto ev = fromProto(re.chat_message()); delegate_snapshot->onChatMessageReceived(*this, ev); @@ -497,36 +950,33 @@ void Room::OnEvent(const FfiEvent &event) { break; } case proto::RoomEvent::kParticipantsUpdated: { - auto ev = fromProto(re.participants_updated()); + ParticipantsUpdatedEvent ev; { std::lock_guard guard(lock_); const auto &pu = re.participants_updated(); for (const auto &info : pu.participants()) { const std::string &identity = info.identity(); Participant *participant = nullptr; - // First, check local participant. + if (local_participant_ && identity == local_participant_->identity()) { participant = local_participant_.get(); } else { - // Otherwise, look for a remote participant. auto it = remote_participants_.find(identity); if (it != remote_participants_.end()) { participant = it->second.get(); } } - if (!participant) { - // Participant might not exist yet; ignore for now. std::cerr << "Room::RoomEvent::kParticipantsUpdated participant " "does not exist: " << identity << std::endl; continue; } - // Update basic fields participant->set_name(info.name()); participant->set_metadata(info.metadata()); + std::unordered_map attrs; attrs.reserve(info.attributes_size()); for (const auto &kv : info.attributes()) { @@ -536,6 +986,8 @@ void Room::OnEvent(const FfiEvent &event) { participant->set_kind(fromProto(info.kind())); participant->set_disconnect_reason( toDisconnectReason(info.disconnect_reason())); + + ev.participants.push_back(participant); } } delegate_snapshot->onParticipantsUpdated(*this, ev); diff --git a/src/room_event_converter.h b/src/room_event_converter.h deleted file mode 100644 index f8dde1f..0000000 --- a/src/room_event_converter.h +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Copyright 2023 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 "livekit/room_delegate.h" -#include "room.pb.h" - -namespace livekit { - -// --------- basic helper conversions --------- - -ConnectionQuality toConnectionQuality(proto::ConnectionQuality src); -ConnectionState toConnectionState(proto::ConnectionState src); -DataPacketKind toDataPacketKind(proto::DataPacketKind src); -EncryptionState toEncryptionState(proto::EncryptionState src); -DisconnectReason toDisconnectReason(proto::DisconnectReason src); - -TranscriptionSegmentData fromProto(const proto::TranscriptionSegment &src); -ChatMessageData fromProto(const proto::ChatMessage &src); -UserPacketData fromProto(const proto::UserPacket &src); -SipDtmfData fromProto(const proto::SipDTMF &src); -RoomInfoData fromProto(const proto::RoomInfo &src); -AttributeEntry fromProto(const proto::AttributesEntry &src); - -DataStreamHeaderData fromProto(const proto::DataStream_Header &src); -DataStreamChunkData fromProto(const proto::DataStream_Chunk &src); -DataStreamTrailerData fromProto(const proto::DataStream_Trailer &src); - -// --------- event conversions (RoomEvent.oneof message) --------- - -ParticipantConnectedEvent fromProto(const proto::ParticipantConnected &src); -ParticipantDisconnectedEvent -fromProto(const proto::ParticipantDisconnected &src); - -LocalTrackPublishedEvent fromProto(const proto::LocalTrackPublished &src); -LocalTrackUnpublishedEvent fromProto(const proto::LocalTrackUnpublished &src); -LocalTrackSubscribedEvent fromProto(const proto::LocalTrackSubscribed &src); - -TrackPublishedEvent fromProto(const proto::TrackPublished &src); -TrackUnpublishedEvent fromProto(const proto::TrackUnpublished &src); -TrackUnsubscribedEvent fromProto(const proto::TrackUnsubscribed &src); -TrackSubscriptionFailedEvent -fromProto(const proto::TrackSubscriptionFailed &src); -TrackMutedEvent fromProto(const proto::TrackMuted &src); -TrackUnmutedEvent fromProto(const proto::TrackUnmuted &src); - -ActiveSpeakersChangedEvent fromProto(const proto::ActiveSpeakersChanged &src); - -RoomMetadataChangedEvent fromProto(const proto::RoomMetadataChanged &src); -RoomSidChangedEvent fromProto(const proto::RoomSidChanged &src); - -ParticipantMetadataChangedEvent -fromProto(const proto::ParticipantMetadataChanged &src); -ParticipantNameChangedEvent fromProto(const proto::ParticipantNameChanged &src); -ParticipantAttributesChangedEvent -fromProto(const proto::ParticipantAttributesChanged &src); -ParticipantEncryptionStatusChangedEvent -fromProto(const proto::ParticipantEncryptionStatusChanged &src); - -ConnectionQualityChangedEvent -fromProto(const proto::ConnectionQualityChanged &src); - -DataPacketReceivedEvent fromProto(const proto::DataPacketReceived &src); -TranscriptionReceivedEvent fromProto(const proto::TranscriptionReceived &src); - -ConnectionStateChangedEvent fromProto(const proto::ConnectionStateChanged &src); -DisconnectedEvent fromProto(const proto::Disconnected &src); -ReconnectingEvent fromProto(const proto::Reconnecting &src); -ReconnectedEvent fromProto(const proto::Reconnected &src); -RoomEosEvent fromProto(const proto::RoomEOS &src); - -DataStreamHeaderReceivedEvent -fromProto(const proto::DataStreamHeaderReceived &src); -DataStreamChunkReceivedEvent -fromProto(const proto::DataStreamChunkReceived &src); -DataStreamTrailerReceivedEvent -fromProto(const proto::DataStreamTrailerReceived &src); - -DataChannelBufferedAmountLowThresholdChangedEvent -fromProto(const proto::DataChannelBufferedAmountLowThresholdChanged &src); - -ByteStreamOpenedEvent fromProto(const proto::ByteStreamOpened &src); -TextStreamOpenedEvent fromProto(const proto::TextStreamOpened &src); - -RoomUpdatedEvent -roomUpdatedFromProto(const proto::RoomInfo &src); // room_updated -RoomMovedEvent roomMovedFromProto(const proto::RoomInfo &src); // moved - -ParticipantsUpdatedEvent fromProto(const proto::ParticipantsUpdated &src); -E2eeStateChangedEvent fromProto(const proto::E2eeStateChanged &src); -ChatMessageReceivedEvent fromProto(const proto::ChatMessageReceived &src); - -} // namespace livekit From b584aa1849ea49916fe14fdd80706928b59e0222 Mon Sep 17 00:00:00 2001 From: shijing xian Date: Mon, 8 Dec 2025 20:50:01 -0800 Subject: [PATCH 06/12] fix the build From 24a207245156153b57048ccd4a84ab0299eecc08 Mon Sep 17 00:00:00 2001 From: shijing xian Date: Mon, 8 Dec 2025 20:54:49 -0800 Subject: [PATCH 07/12] recover the room.cpp From 2b21e655c8b2a53f1e12c0eb2f1143c3467acba3 Mon Sep 17 00:00:00 2001 From: shijing xian Date: Mon, 8 Dec 2025 20:56:16 -0800 Subject: [PATCH 08/12] recover room.cpp from the uncheckin changes --- src/room.cpp | 590 ++++++--------------------------------------------- 1 file changed, 69 insertions(+), 521 deletions(-) diff --git a/src/room.cpp b/src/room.cpp index 4f29583..c20aac1 100644 --- a/src/room.cpp +++ b/src/room.cpp @@ -45,7 +45,7 @@ using proto::FfiResponse; namespace { -std::shared_ptr +std::unique_ptr createRemoteParticipant(const proto::OwnedParticipant &owned) { const auto &pinfo = owned.info(); std::unordered_map attrs; @@ -56,7 +56,7 @@ createRemoteParticipant(const proto::OwnedParticipant &owned) { auto kind = livekit::fromProto(pinfo.kind()); auto reason = livekit::toDisconnectReason(pinfo.disconnect_reason()); livekit::FfiHandle handle(static_cast(owned.handle().id())); - return std::make_shared( + return std::make_unique( std::move(handle), pinfo.sid(), pinfo.name(), pinfo.identity(), pinfo.metadata(), std::move(attrs), kind, reason); } @@ -125,8 +125,8 @@ bool Room::Connect(const std::string &url, const std::string &token, for (const auto &owned_publication_info : pt.publications()) { auto publication = std::make_shared(owned_publication_info); - rp->mutableTrackPublications().emplace(publication->sid(), - std::move(publication)); + rp->mutable_track_publications().emplace(publication->sid(), + std::move(publication)); } remote_participants_.emplace(rp->identity(), std::move(rp)); @@ -210,34 +210,27 @@ void Room::OnEvent(const FfiEvent &event) { switch (re.message_case()) { case proto::RoomEvent::kParticipantConnected: { - std::shared_ptr new_participant; + auto ev = fromProto(re.participant_connected()); + std::cout << "kParticipantConnected " << std::endl; + // Create and register RemoteParticipant { std::lock_guard guard(lock_); - const auto &owned = re.participant_connected().info(); - // createRemoteParticipant takes proto::OwnedParticipant - new_participant = createRemoteParticipant(owned); - remote_participants_.emplace(new_participant->identity(), - new_participant); + auto rp = createRemoteParticipant(re.participant_connected().info()); + remote_participants_.emplace(rp->identity(), std::move(rp)); } - ParticipantConnectedEvent ev; - ev.participant = new_participant.get(); + // TODO, use better public callback events delegate_snapshot->onParticipantConnected(*this, ev); break; } case proto::RoomEvent::kParticipantDisconnected: { - std::shared_ptr removed; - DisconnectReason reason = DisconnectReason::Unknown; - + auto ev = fromProto(re.participant_disconnected()); { std::lock_guard guard(lock_); const auto &pd = re.participant_disconnected(); const std::string &identity = pd.participant_identity(); - reason = toDisconnectReason(pd.disconnect_reason()); - auto it = remote_participants_.find(identity); if (it != remote_participants_.end()) { - removed = it->second; remote_participants_.erase(it); } else { // We saw a disconnect event for a participant we don't track @@ -247,86 +240,28 @@ void Room::OnEvent(const FfiEvent &event) { << identity << std::endl; } } - if (removed) { - ParticipantDisconnectedEvent ev; - ev.participant = removed.get(); - ev.reason = reason; - delegate_snapshot->onParticipantDisconnected(*this, ev); - } + // TODO, should we trigger onParticipantDisconnected if remote + // participants can't be found ? + delegate_snapshot->onParticipantDisconnected(*this, ev); break; } case proto::RoomEvent::kLocalTrackPublished: { - LocalTrackPublishedEvent ev; - { - std::lock_guard guard(lock_); - if (!local_participant_) { - std::cerr << "kLocalTrackPublished: local_participant_ is nullptr" - << std::endl; - break; - } - const auto <p = re.local_track_published(); - const std::string &sid = ltp.track_sid(); - auto &pubs = local_participant_->trackPublications(); - auto it = pubs.find(sid); - if (it == pubs.end()) { - std::cerr << "local_track_published for unknown sid: " << sid - << std::endl; - break; - } - ev.publication = it->second; - ev.track = ev.publication ? ev.publication->track() : nullptr; - } + auto ev = fromProto(re.local_track_published()); delegate_snapshot->onLocalTrackPublished(*this, ev); break; } case proto::RoomEvent::kLocalTrackUnpublished: { - LocalTrackUnpublishedEvent ev; - { - std::lock_guard guard(lock_); - if (!local_participant_) { - std::cerr << "kLocalTrackPublished: local_participant_ is nullptr" - << std::endl; - break; - } - const auto <u = re.local_track_unpublished(); - const std::string &pub_sid = ltu.publication_sid(); - auto &pubs = local_participant_->trackPublications(); - auto it = pubs.find(pub_sid); - if (it == pubs.end()) { - std::cerr << "local_track_unpublished for unknown publication sid: " - << pub_sid << std::endl; - break; - } - ev.publication = it->second; - } + auto ev = fromProto(re.local_track_unpublished()); delegate_snapshot->onLocalTrackUnpublished(*this, ev); break; } case proto::RoomEvent::kLocalTrackSubscribed: { - LocalTrackSubscribedEvent ev; - { - std::lock_guard guard(lock_); - if (!local_participant_) { - break; - } - const auto <s = re.local_track_subscribed(); - const std::string &sid = lts.track_sid(); - auto &pubs = local_participant_->trackPublications(); - auto it = pubs.find(sid); - if (it == pubs.end()) { - std::cerr << "local_track_subscribed for unknown sid: " << sid - << std::endl; - break; - } - auto publication = it->second; - ev.track = publication ? publication->track() : nullptr; - } - + auto ev = fromProto(re.local_track_subscribed()); delegate_snapshot->onLocalTrackSubscribed(*this, ev); break; } case proto::RoomEvent::kTrackPublished: { - TrackPublishedEvent ev; + auto ev = fromProto(re.track_published()); { std::lock_guard guard(lock_); const auto &tp = re.track_published(); @@ -338,14 +273,13 @@ void Room::OnEvent(const FfiEvent &event) { auto rpublication = std::make_shared(owned_publication); // Store it on the participant, keyed by SID - rparticipant->mutableTrackPublications().emplace( + rparticipant->mutable_track_publications().emplace( rpublication->sid(), std::move(rpublication)); - ev.participant = rparticipant; - ev.publication = rpublication; + } else { // Optional: log if we get a track for an unknown participant std::cerr << "track_published for unknown participant: " << identity - << std::endl; + << "\n"; // Don't emit the break; } @@ -354,31 +288,7 @@ void Room::OnEvent(const FfiEvent &event) { break; } case proto::RoomEvent::kTrackUnpublished: { - TrackUnpublishedEvent ev; - { - std::lock_guard guard(lock_); - const auto &tu = re.track_unpublished(); - const std::string &identity = tu.participant_identity(); - const std::string &pub_sid = tu.publication_sid(); - auto pit = remote_participants_.find(identity); - if (pit == remote_participants_.end()) { - std::cerr << "track_unpublished for unknown participant: " << identity - << std::endl; - break; - } - RemoteParticipant *rparticipant = pit->second.get(); - auto &pubs = rparticipant->mutableTrackPublications(); - auto it = pubs.find(pub_sid); - if (it == pubs.end()) { - std::cerr << "track_unpublished for unknown publication sid " - << pub_sid << " (participant " << identity << ")\n"; - break; - } - ev.participant = rparticipant; - ev.publication = it->second; - pubs.erase(it); - } - + auto ev = fromProto(re.track_unpublished()); delegate_snapshot->onTrackUnpublished(*this, ev); break; } @@ -401,7 +311,7 @@ void Room::OnEvent(const FfiEvent &event) { } rparticipant = pit->second.get(); // Find existing publication by track SID (from track_published) - auto &pubs = rparticipant->mutableTrackPublications(); + auto &pubs = rparticipant->mutable_track_publications(); auto pubIt = pubs.find(track_info.sid()); if (pubIt == pubs.end()) { std::cerr << "track_subscribed for unknown publication sid " @@ -421,9 +331,13 @@ void Room::OnEvent(const FfiEvent &event) { << track_info.kind() << "\n"; break; } + std::cout << "before setTrack " << std::endl; + // Attach to publication, mark subscribed rpublication->setTrack(remote_track); + std::cout << "setTrack " << std::endl; rpublication->setSubscribed(true); + std::cout << "setSubscribed " << std::endl; } // Emit remote track_subscribed-style callback @@ -431,478 +345,111 @@ void Room::OnEvent(const FfiEvent &event) { ev.track = remote_track; ev.publication = rpublication; ev.participant = rparticipant; + std::cout << "onTrackSubscribed " << std::endl; delegate_snapshot->onTrackSubscribed(*this, ev); + std::cout << "after onTrackSubscribed " << std::endl; break; } case proto::RoomEvent::kTrackUnsubscribed: { - TrackUnsubscribedEvent ev; - { - std::lock_guard guard(lock_); - const auto &tu = re.track_unsubscribed(); - const std::string &identity = tu.participant_identity(); - const std::string &track_sid = tu.track_sid(); - auto pit = remote_participants_.find(identity); - if (pit == remote_participants_.end()) { - std::cerr << "track_unsubscribed for unknown participant: " - << identity << "\n"; - break; - } - RemoteParticipant *rparticipant = pit->second.get(); - auto &pubs = rparticipant->mutableTrackPublications(); - auto pubIt = pubs.find(track_sid); - if (pubIt == pubs.end()) { - std::cerr << "track_unsubscribed for unknown publication sid " - << track_sid << " (participant " << identity << ")\n"; - break; - } - auto publication = pubIt->second; - auto track = publication->track(); - publication->setTrack(nullptr); - publication->setSubscribed(false); - ev.participant = rparticipant; - ev.publication = publication; - ev.track = track; - } - + auto ev = fromProto(re.track_unsubscribed()); delegate_snapshot->onTrackUnsubscribed(*this, ev); break; } case proto::RoomEvent::kTrackSubscriptionFailed: { - TrackSubscriptionFailedEvent ev; - { - std::lock_guard guard(lock_); - const auto &tsf = re.track_subscription_failed(); - const std::string &identity = tsf.participant_identity(); - auto pit = remote_participants_.find(identity); - if (pit == remote_participants_.end()) { - std::cerr << "track_subscription_failed for unknown participant: " - << identity << "\n"; - break; - } - ev.participant = pit->second.get(); - ev.track_sid = tsf.track_sid(); - ev.error = tsf.error(); - } + auto ev = fromProto(re.track_subscription_failed()); delegate_snapshot->onTrackSubscriptionFailed(*this, ev); break; } case proto::RoomEvent::kTrackMuted: { - TrackMutedEvent ev; - bool success = false; - { - std::lock_guard guard(lock_); - const auto &tm = re.track_muted(); - const std::string &identity = tm.participant_identity(); - const std::string &sid = tm.track_sid(); - Participant *participant = nullptr; - if (local_participant_ && local_participant_->identity() == identity) { - participant = local_participant_.get(); - } else { - auto pit = remote_participants_.find(identity); - if (pit != remote_participants_.end()) { - participant = pit->second.get(); - } - } - if (!participant) { - std::cerr << "track_muted for unknown participant: " << identity - << "\n"; - break; - } - auto pub = participant->findTrackPublication(sid); - if (!pub) { - std::cerr << "track_muted for unknown track sid: " << sid - << std::endl; - } else { - pub->setMuted(true); - if (auto t = pub->track()) { - t->setMuted(true); - } - ev.participant = participant; - ev.publication = pub; - success = true; - } - } - if (success) { - delegate_snapshot->onTrackMuted(*this, ev); - } + auto ev = fromProto(re.track_muted()); + delegate_snapshot->onTrackMuted(*this, ev); break; } case proto::RoomEvent::kTrackUnmuted: { - TrackUnmutedEvent ev; - bool success = false; - { - std::lock_guard guard(lock_); - const auto &tu = re.track_unmuted(); - const std::string &identity = tu.participant_identity(); - const std::string &sid = tu.track_sid(); - Participant *participant = nullptr; - if (local_participant_ && local_participant_->identity() == identity) { - participant = local_participant_.get(); - } else { - auto pit = remote_participants_.find(identity); - if (pit != remote_participants_.end()) { - participant = pit->second.get(); - } - } - if (!participant) { - std::cerr << "track_unmuted for unknown participant: " << identity - << "\n"; - break; - } - - auto pub = participant->findTrackPublication(sid); - if (!pub) { - std::cerr << "track_muted for unknown track sid: " << sid - << std::endl; - } else { - pub->setMuted(false); - if (auto t = pub->track()) { - t->setMuted(false); - } - ev.participant = participant; - ev.publication = pub; - success = true; - } - - ev.participant = participant; - ev.publication = pub; - } - - if (success) { - delegate_snapshot->onTrackUnmuted(*this, ev); - } + auto ev = fromProto(re.track_unmuted()); + delegate_snapshot->onTrackUnmuted(*this, ev); break; } case proto::RoomEvent::kActiveSpeakersChanged: { - ActiveSpeakersChangedEvent ev; - { - std::lock_guard guard(lock_); - const auto &asc = re.active_speakers_changed(); - for (const auto &identity : asc.participant_identities()) { - Participant *participant = nullptr; - if (local_participant_ && - local_participant_->identity() == identity) { - participant = local_participant_.get(); - } else { - auto pit = remote_participants_.find(identity); - if (pit != remote_participants_.end()) { - participant = pit->second.get(); - } - } - if (participant) { - ev.speakers.push_back(participant); - } - } - } + auto ev = fromProto(re.active_speakers_changed()); delegate_snapshot->onActiveSpeakersChanged(*this, ev); break; } case proto::RoomEvent::kRoomMetadataChanged: { - RoomMetadataChangedEvent ev; - { - std::lock_guard guard(lock_); - const auto old_metadata = room_info_.metadata; - room_info_.metadata = re.room_metadata_changed().metadata(); - ev.old_metadata = old_metadata; - ev.new_metadata = room_info_.metadata; - } + auto ev = fromProto(re.room_metadata_changed()); delegate_snapshot->onRoomMetadataChanged(*this, ev); break; } case proto::RoomEvent::kRoomSidChanged: { - RoomSidChangedEvent ev; - { - std::lock_guard guard(lock_); - room_info_.sid = re.room_sid_changed().sid(); - ev.sid = room_info_.sid.value_or(std::string{}); - } + auto ev = fromProto(re.room_sid_changed()); delegate_snapshot->onRoomSidChanged(*this, ev); break; } case proto::RoomEvent::kParticipantMetadataChanged: { - ParticipantMetadataChangedEvent ev; - { - std::lock_guard guard(lock_); - const auto &pm = re.participant_metadata_changed(); - const std::string &identity = pm.participant_identity(); - Participant *participant = nullptr; - if (local_participant_ && local_participant_->identity() == identity) { - participant = local_participant_.get(); - } else { - auto it = remote_participants_.find(identity); - if (it != remote_participants_.end()) { - participant = it->second.get(); - } - } - if (!participant) { - std::cerr << "participant_metadata_changed for unknown participant: " - << identity << "\n"; - break; - } - std::string old_metadata = participant->metadata(); - participant->set_metadata(pm.metadata()); - ev.participant = participant; - ev.old_metadata = old_metadata; - ev.new_metadata = participant->metadata(); - } - + auto ev = fromProto(re.participant_metadata_changed()); delegate_snapshot->onParticipantMetadataChanged(*this, ev); break; } case proto::RoomEvent::kParticipantNameChanged: { - ParticipantNameChangedEvent ev; - { - std::lock_guard guard(lock_); - const auto &pn = re.participant_name_changed(); - const std::string &identity = pn.participant_identity(); - Participant *participant = nullptr; - if (local_participant_ && local_participant_->identity() == identity) { - participant = local_participant_.get(); - } else { - auto it = remote_participants_.find(identity); - if (it != remote_participants_.end()) { - participant = it->second.get(); - } - } - if (!participant) { - std::cerr << "participant_name_changed for unknown participant: " - << identity << "\n"; - break; - } - std::string old_name = participant->name(); - participant->set_name(pn.name()); - ev.participant = participant; - ev.old_name = old_name; - ev.new_name = participant->name(); - } + auto ev = fromProto(re.participant_name_changed()); delegate_snapshot->onParticipantNameChanged(*this, ev); break; } case proto::RoomEvent::kParticipantAttributesChanged: { - ParticipantAttributesChangedEvent ev; - { - std::lock_guard guard(lock_); - const auto &pa = re.participant_attributes_changed(); - const std::string &identity = pa.participant_identity(); - Participant *participant = nullptr; - if (local_participant_ && local_participant_->identity() == identity) { - participant = local_participant_.get(); - } else { - auto it = remote_participants_.find(identity); - if (it != remote_participants_.end()) { - participant = it->second.get(); - } - } - if (!participant) { - std::cerr - << "participant_attributes_changed for unknown participant: " - << identity << "\n"; - break; - } - // Build full attributes map - std::unordered_map attrs; - for (const auto &entry : pa.attributes()) { - attrs.emplace(entry.key(), entry.value()); - } - participant->set_attributes(attrs); - - // Build changed_attributes map - for (const auto &entry : pa.changed_attributes()) { - ev.changed_attributes.emplace_back(entry.key(), entry.value()); - } - ev.participant = participant; - } + auto ev = fromProto(re.participant_attributes_changed()); delegate_snapshot->onParticipantAttributesChanged(*this, ev); break; } case proto::RoomEvent::kParticipantEncryptionStatusChanged: { - ParticipantEncryptionStatusChangedEvent ev; - { - std::lock_guard guard(lock_); - const auto &pe = re.participant_encryption_status_changed(); - const std::string &identity = pe.participant_identity(); - Participant *participant = nullptr; - if (local_participant_ && local_participant_->identity() == identity) { - participant = local_participant_.get(); - } else { - auto it = remote_participants_.find(identity); - if (it != remote_participants_.end()) { - participant = it->second.get(); - } - } - if (!participant) { - std::cerr << "participant_encryption_status_changed for unknown " - "participant: " - << identity << "\n"; - break; - } - ev.participant = participant; - ev.is_encrypted = pe.is_encrypted(); - } - + auto ev = fromProto(re.participant_encryption_status_changed()); delegate_snapshot->onParticipantEncryptionStatusChanged(*this, ev); break; } case proto::RoomEvent::kConnectionQualityChanged: { - ConnectionQualityChangedEvent ev; - { - std::lock_guard guard(lock_); - const auto &cq = re.connection_quality_changed(); - const std::string &identity = cq.participant_identity(); - Participant *participant = nullptr; - if (local_participant_ && local_participant_->identity() == identity) { - participant = local_participant_.get(); - } else { - auto it = remote_participants_.find(identity); - if (it != remote_participants_.end()) { - participant = it->second.get(); - } - } - if (!participant) { - std::cerr << "connection_quality_changed for unknown participant: " - << identity << "\n"; - break; - } - ev.participant = participant; - ev.quality = static_cast(cq.quality()); - } - + auto ev = fromProto(re.connection_quality_changed()); delegate_snapshot->onConnectionQualityChanged(*this, ev); break; } - - // ------------------------------------------------------------------------ - // Transcription - // ------------------------------------------------------------------------ - - case proto::RoomEvent::kTranscriptionReceived: { - TranscriptionReceivedEvent ev; - { - std::lock_guard guard(lock_); - const auto &tr = re.transcription_received(); - for (const auto &s : tr.segments()) { - TranscriptionSegment seg; - seg.id = s.id(); - seg.text = s.text(); - seg.final = s.final(); - seg.start_time = s.start_time(); - seg.end_time = s.end_time(); - seg.language = s.language(); - ev.segments.push_back(std::move(seg)); - } - - Participant *participant = nullptr; - if (!tr.participant_identity().empty()) { - const std::string &identity = tr.participant_identity(); - if (local_participant_ && - local_participant_->identity() == identity) { - participant = local_participant_.get(); - } else { - auto it = remote_participants_.find(identity); - if (it != remote_participants_.end()) { - participant = it->second.get(); - } - } - } - ev.participant = participant; - ev.publication = participant->findTrackPublication(tr.track_sid()); - } - - delegate_snapshot->onTranscriptionReceived(*this, ev); - break; - } - - // ------------------------------------------------------------------------ - // Data packets: user vs SIP DTMF - // ------------------------------------------------------------------------ - case proto::RoomEvent::kDataPacketReceived: { - const auto &dp = re.data_packet_received(); - RemoteParticipant *rp = nullptr; - { - std::lock_guard guard(lock_); - auto it = remote_participants_.find(dp.participant_identity()); - if (it != remote_participants_.end()) { - rp = it->second.get(); - } - } - const auto which_val = dp.value_case(); - if (which_val == proto::DataPacketReceived::kUser) { - UserDataPacketEvent ev = userDataPacketFromProto(dp, rp); - delegate_snapshot->onUserPacketReceived(*this, ev); - } else if (which_val == proto::DataPacketReceived::kSipDtmf) { - SipDtmfReceivedEvent ev = sipDtmfFromProto(dp, rp); - delegate_snapshot->onSipDtmfReceived(*this, ev); - } - break; - } - - // ------------------------------------------------------------------------ - // E2EE state - // ------------------------------------------------------------------------ - case proto::RoomEvent::kE2EeStateChanged: { - E2eeStateChangedEvent ev; - { - std::lock_guard guard(lock_); - const auto &es = re.e2ee_state_changed(); - const std::string &identity = es.participant_identity(); - Participant *participant = nullptr; - if (local_participant_ && local_participant_->identity() == identity) { - participant = local_participant_.get(); - } else { - auto it = remote_participants_.find(identity); - if (it != remote_participants_.end()) { - participant = it->second.get(); - } - } - if (!participant) { - std::cerr << "e2ee_state_changed for unknown participant: " - << identity << std::endl; - break; - } - - ev.participant = participant; - ev.state = static_cast(es.state()); - } - delegate_snapshot->onE2eeStateChanged(*this, ev); - break; - } - - // ------------------------------------------------------------------------ - // Connection state / lifecycle - // ------------------------------------------------------------------------ - case proto::RoomEvent::kConnectionStateChanged: { - ConnectionStateChangedEvent ev; - { - std::lock_guard guard(lock_); - const auto &cs = re.connection_state_changed(); - connection_state_ = static_cast(cs.state()); - ev.state = connection_state_; - } + auto ev = fromProto(re.connection_state_changed()); delegate_snapshot->onConnectionStateChanged(*this, ev); break; } case proto::RoomEvent::kDisconnected: { - DisconnectedEvent ev; - ev.reason = toDisconnectReason(re.disconnected().reason()); + auto ev = fromProto(re.disconnected()); delegate_snapshot->onDisconnected(*this, ev); break; } case proto::RoomEvent::kReconnecting: { - ReconnectingEvent ev; + auto ev = fromProto(re.reconnecting()); delegate_snapshot->onReconnecting(*this, ev); break; } case proto::RoomEvent::kReconnected: { - ReconnectedEvent ev; + auto ev = fromProto(re.reconnected()); delegate_snapshot->onReconnected(*this, ev); break; } + case proto::RoomEvent::kE2EeStateChanged: { + auto ev = fromProto(re.e2ee_state_changed()); + delegate_snapshot->onE2eeStateChanged(*this, ev); + break; + } case proto::RoomEvent::kEos: { - RoomEosEvent ev; + auto ev = fromProto(re.eos()); delegate_snapshot->onRoomEos(*this, ev); break; } + case proto::RoomEvent::kDataPacketReceived: { + auto ev = fromProto(re.data_packet_received()); + delegate_snapshot->onDataPacketReceived(*this, ev); + break; + } + case proto::RoomEvent::kTranscriptionReceived: { + auto ev = fromProto(re.transcription_received()); + delegate_snapshot->onTranscriptionReceived(*this, ev); + break; + } case proto::RoomEvent::kChatMessage: { auto ev = fromProto(re.chat_message()); delegate_snapshot->onChatMessageReceived(*this, ev); @@ -950,33 +497,36 @@ void Room::OnEvent(const FfiEvent &event) { break; } case proto::RoomEvent::kParticipantsUpdated: { - ParticipantsUpdatedEvent ev; + auto ev = fromProto(re.participants_updated()); { std::lock_guard guard(lock_); const auto &pu = re.participants_updated(); for (const auto &info : pu.participants()) { const std::string &identity = info.identity(); Participant *participant = nullptr; - + // First, check local participant. if (local_participant_ && identity == local_participant_->identity()) { participant = local_participant_.get(); } else { + // Otherwise, look for a remote participant. auto it = remote_participants_.find(identity); if (it != remote_participants_.end()) { participant = it->second.get(); } } + if (!participant) { + // Participant might not exist yet; ignore for now. std::cerr << "Room::RoomEvent::kParticipantsUpdated participant " "does not exist: " << identity << std::endl; continue; } + // Update basic fields participant->set_name(info.name()); participant->set_metadata(info.metadata()); - std::unordered_map attrs; attrs.reserve(info.attributes_size()); for (const auto &kv : info.attributes()) { @@ -986,8 +536,6 @@ void Room::OnEvent(const FfiEvent &event) { participant->set_kind(fromProto(info.kind())); participant->set_disconnect_reason( toDisconnectReason(info.disconnect_reason())); - - ev.participants.push_back(participant); } } delegate_snapshot->onParticipantsUpdated(*this, ev); From 6f8756d70d122a0a516e4440473127e9af5fd3ef Mon Sep 17 00:00:00 2001 From: shijing xian Date: Mon, 8 Dec 2025 20:58:46 -0800 Subject: [PATCH 09/12] initial commit --- examples/simple_room/main.cpp | 12 +- include/livekit/audio_frame.h | 1 + include/livekit/audio_source.h | 3 +- include/livekit/audio_stream.h | 2 +- include/livekit/local_participant.h | 3 + include/livekit/participant.h | 6 + include/livekit/remote_participant.h | 15 +- include/livekit/room.h | 1 + include/livekit/room_delegate.h | 126 +++--- include/livekit/track_publication.h | 2 +- include/livekit/video_frame.h | 1 + include/livekit/video_source.h | 4 +- include/livekit/video_stream.h | 2 +- src/audio_source.cpp | 5 - src/local_participant.cpp | 9 + src/remote_participant.cpp | 9 + src/room.cpp | 590 +++++++++++++++++++++++---- src/room_proto_converter.cpp | 229 ++--------- src/room_proto_converter.h | 24 +- src/video_stream.cpp | 4 - 20 files changed, 685 insertions(+), 363 deletions(-) diff --git a/examples/simple_room/main.cpp b/examples/simple_room/main.cpp index 9338e2f..5741cc5 100644 --- a/examples/simple_room/main.cpp +++ b/examples/simple_room/main.cpp @@ -156,8 +156,9 @@ class SimpleRoomDelegate : public livekit::RoomDelegate { void onParticipantConnected( livekit::Room & /*room*/, const livekit::ParticipantConnectedEvent &ev) override { - std::cout << "[Room] participant connected: identity=" << ev.identity - << " name=" << ev.name << "\n"; + std::cout << "[Room] participant connected: identity=" + << ev.participant->identity() + << " name=" << ev.participant->name() << "\n"; } void onTrackSubscribed(livekit::Room & /*room*/, @@ -172,19 +173,18 @@ class SimpleRoomDelegate : public livekit::RoomDelegate { << participant_identity << " track_sid=" << track_sid << " name=" << track_name; if (ev.track) { - std::cout << " kind=" << static_cast(ev.track->kind()) << "\n"; + std::cout << " kind=" << static_cast(ev.track->kind()); } if (ev.publication) { - std::cout << " source=" << static_cast(ev.publication->source()) - << "\n"; + std::cout << " source=" << static_cast(ev.publication->source()); } + std::cout << std::endl; // If this is a VIDEO track, create a VideoStream and attach to renderer if (ev.track && ev.track->kind() == TrackKind::KIND_VIDEO) { VideoStream::Options opts; opts.format = livekit::VideoBufferType::RGBA; auto video_stream = VideoStream::fromTrack(ev.track, opts); - std::cout << "after fromTrack " << std::endl; if (!video_stream) { std::cerr << "Failed to create VideoStream for track " << track_sid << "\n"; diff --git a/include/livekit/audio_frame.h b/include/livekit/audio_frame.h index 529d658..a7011c9 100644 --- a/include/livekit/audio_frame.h +++ b/include/livekit/audio_frame.h @@ -43,6 +43,7 @@ class AudioFrame { AudioFrame(std::vector data, int sample_rate, int num_channels, int samples_per_channel); AudioFrame(); // Default constructor + virtual ~AudioFrame() = default; /** * Create a new zero-initialized AudioFrame instance. diff --git a/include/livekit/audio_source.h b/include/livekit/audio_source.h index 8e52bbd..e3c864a 100644 --- a/include/livekit/audio_source.h +++ b/include/livekit/audio_source.h @@ -43,8 +43,7 @@ class AudioSource { * @param queue_size_ms Max buffer duration for the internal queue in ms. */ AudioSource(int sample_rate, int num_channels, int queue_size_ms = 1000); - - ~AudioSource(); + virtual ~AudioSource() = default; AudioSource(const AudioSource &) = delete; AudioSource &operator=(const AudioSource &) = delete; diff --git a/include/livekit/audio_stream.h b/include/livekit/audio_stream.h index 3616e8d..5baefc9 100644 --- a/include/livekit/audio_stream.h +++ b/include/livekit/audio_stream.h @@ -85,7 +85,7 @@ class AudioStream { TrackSource track_source, const Options &options); - ~AudioStream(); + virtual ~AudioStream(); /// No copy, assignment constructors. AudioStream(const AudioStream &) = delete; diff --git a/include/livekit/local_participant.h b/include/livekit/local_participant.h index 17dc9a0..707da54 100644 --- a/include/livekit/local_participant.h +++ b/include/livekit/local_participant.h @@ -203,6 +203,9 @@ class LocalParticipant : public Participant { const std::string &caller_identity, const std::string &payload, double response_timeout); + // Called by Room events like kTrackMuted. + std::shared_ptr + findTrackPublication(const std::string &sid) const override; friend class Room; private: diff --git a/include/livekit/participant.h b/include/livekit/participant.h index 0cc7418..9b82b28 100644 --- a/include/livekit/participant.h +++ b/include/livekit/participant.h @@ -39,6 +39,7 @@ class Participant { name_(std::move(name)), identity_(std::move(identity)), metadata_(std::move(metadata)), attributes_(std::move(attributes)), kind_(kind), reason_(reason) {} + virtual ~Participant() = default; // Plain getters (caller ensures threading) const std::string &sid() const noexcept { return sid_; } @@ -72,6 +73,11 @@ class Participant { reason_ = reason; } +protected: + virtual std::shared_ptr + findTrackPublication(const std::string &sid) const = 0; + friend class Room; + private: FfiHandle handle_; std::string sid_, name_, identity_, metadata_; diff --git a/include/livekit/remote_participant.h b/include/livekit/remote_participant.h index d1c1579..cbd8aa3 100644 --- a/include/livekit/remote_participant.h +++ b/include/livekit/remote_participant.h @@ -28,7 +28,7 @@ class RemoteTrackPublication; class RemoteParticipant : public Participant { public: - using TrackPublicationMap = + using PublicationMap = std::unordered_map>; RemoteParticipant(FfiHandle handle, std::string sid, std::string name, @@ -37,20 +37,27 @@ class RemoteParticipant : public Participant { ParticipantKind kind, DisconnectReason reason); // A dictionary of track publications associated with the participant. - const TrackPublicationMap &track_publications() const noexcept { + const PublicationMap &trackPublications() const noexcept { return track_publications_; } // Optional: non-const access if you want to mutate in-place. - TrackPublicationMap &mutable_track_publications() noexcept { + PublicationMap &mutableTrackPublications() noexcept { return track_publications_; } // C++ equivalent of Python's __repr__ std::string to_string() const; +protected: + // Called by Room events like kTrackMuted. This is internal plumbing and not + // intended to be called directly by SDK users. + std::shared_ptr + findTrackPublication(const std::string &sid) const override; + friend class Room; + private: - TrackPublicationMap track_publications_; + PublicationMap track_publications_; }; // Convenience for logging / streaming diff --git a/include/livekit/room.h b/include/livekit/room.h index f2e21d4..d244dd7 100644 --- a/include/livekit/room.h +++ b/include/livekit/room.h @@ -185,6 +185,7 @@ class Room { std::unique_ptr local_participant_; std::unordered_map> remote_participants_; + ConnectionState connection_state_ = ConnectionState::Disconnected; void OnEvent(const proto::FfiEvent &event); }; diff --git a/include/livekit/room_delegate.h b/include/livekit/room_delegate.h index 0e73fae..7237ea6 100644 --- a/include/livekit/room_delegate.h +++ b/include/livekit/room_delegate.h @@ -23,24 +23,29 @@ #include #include +#include "livekit/local_track_publication.h" +#include "livekit/remote_track_publication.h" +#include "livekit/track_publication.h" + namespace livekit { class Room; enum class VideoCodec; enum class TrackSource; class Track; -class RemoteTrackPublication; class RemoteParticipant; +class LocalTrackPublication; +class Participant; enum class ConnectionQuality { - Poor, + Poor = 0, Good, Excellent, Lost, }; enum class ConnectionState { - Disconnected, + Disconnected = 0, Connected, Reconnecting, }; @@ -51,10 +56,13 @@ enum class DataPacketKind { }; enum class EncryptionState { - // mirror your proto enum values as needed - Unknown, - On, - Off, + New = 0, + Ok, + EncryptionFailed, + DecryptionFailed, + MissingKey, + KeyRatcheted, + InternalError, }; enum class DisconnectReason { @@ -76,10 +84,6 @@ enum class DisconnectReason { MediaFailure }; -// --------------------------------------------------------- -// Basic data types corresponding to proto messages -// --------------------------------------------------------- - struct ChatMessageData { std::string id; std::int64_t timestamp = 0; @@ -117,6 +121,9 @@ struct RoomInfoData { struct AttributeEntry { std::string key; std::string value; + AttributeEntry() = default; + AttributeEntry(std::string k, std::string v) + : key(std::move(k)), value(std::move(v)) {} }; struct DataStreamHeaderData { @@ -204,75 +211,72 @@ struct TranscriptionSegment { // --------------------------------------------------------- struct ParticipantConnectedEvent { - // Typically you’d also attach a handle / participant object - std::string identity; // from OwnedParticipant / ParticipantInfo - std::string metadata; - std::string name; + RemoteParticipant *participant = nullptr; // Owned by room }; struct ParticipantDisconnectedEvent { - std::string participant_identity; + RemoteParticipant *participant = nullptr; // Owned by room DisconnectReason reason = DisconnectReason::Unknown; }; struct LocalTrackPublishedEvent { - std::string track_sid; + std::shared_ptr publication; + std::shared_ptr track; }; struct LocalTrackUnpublishedEvent { - std::string publication_sid; + std::shared_ptr publication; }; struct LocalTrackSubscribedEvent { - std::string track_sid; + std::shared_ptr track; }; struct TrackPublishedEvent { - std::string participant_identity; - std::string publication_sid; - std::string track_name; - std::string track_kind; // or an enum if you have one - std::string track_source; // or enum + std::shared_ptr publication; + RemoteParticipant *participant = nullptr; // Owned by room }; struct TrackUnpublishedEvent { - std::string participant_identity; - std::string publication_sid; + std::shared_ptr publication; + RemoteParticipant *participant = nullptr; }; struct TrackSubscribedEvent { std::shared_ptr track; std::shared_ptr publication; - RemoteParticipant *participant = nullptr; + RemoteParticipant *participant = nullptr; // Owned by room }; struct TrackUnsubscribedEvent { - std::string participant_identity; - std::string track_sid; + std::shared_ptr track; + std::shared_ptr publication; + RemoteParticipant *participant = nullptr; // Owned by room }; struct TrackSubscriptionFailedEvent { - std::string participant_identity; + RemoteParticipant *participant = nullptr; // Owned by room std::string track_sid; std::string error; }; struct TrackMutedEvent { - std::string participant_identity; - std::string track_sid; + Participant *participant = nullptr; // Local or Remote, owned by room + std::shared_ptr publication; }; struct TrackUnmutedEvent { - std::string participant_identity; - std::string track_sid; + Participant *participant = nullptr; // Local or Remote, owned by room + std::shared_ptr publication; }; struct ActiveSpeakersChangedEvent { - std::vector participant_identities; + std::vector speakers; }; struct RoomMetadataChangedEvent { - std::string metadata; + std::string old_metadata; + std::string new_metadata; }; struct RoomSidChangedEvent { @@ -280,36 +284,43 @@ struct RoomSidChangedEvent { }; struct ParticipantMetadataChangedEvent { - std::string participant_identity; - std::string metadata; + Participant *participant = nullptr; // Local or Remote, owned by room + std::string old_metadata; + std::string new_metadata; }; struct ParticipantNameChangedEvent { - std::string participant_identity; - std::string name; + Participant *participant = nullptr; // Local or Remote, owned by room + std::string old_name; + std::string new_name; }; struct ParticipantAttributesChangedEvent { - std::string participant_identity; - std::vector attributes; + Participant *participant = nullptr; // Local or Remote, owned by room std::vector changed_attributes; }; struct ParticipantEncryptionStatusChangedEvent { - std::string participant_identity; + Participant *participant = nullptr; // Local or Remote, owned by room bool is_encrypted = false; }; struct ConnectionQualityChangedEvent { - std::string participant_identity; + Participant *participant = nullptr; // Local or Remote, owned by room ConnectionQuality quality = ConnectionQuality::Good; }; -struct DataPacketReceivedEvent { +struct UserDataPacketEvent { + std::vector data; DataPacketKind kind = DataPacketKind::Reliable; - std::string participant_identity; // may be empty - std::optional user; - std::optional sip_dtmf; + RemoteParticipant *participant = nullptr; // may be null, owned by room + std::string topic; +}; + +struct SipDtmfReceivedEvent { + int code = 0; + std::string digit; + RemoteParticipant *participant = nullptr; // owned by room }; struct Transcription { @@ -318,6 +329,12 @@ struct Transcription { std::vector segments; }; +struct TranscriptionReceivedEvent { + std::vector segments; + Participant *participant = nullptr; // Local or Remote, owned by room + std::shared_ptr publication; +}; + struct ConnectionStateChangedEvent { ConnectionState state = ConnectionState::Disconnected; }; @@ -370,13 +387,12 @@ struct RoomMovedEvent { }; struct ParticipantsUpdatedEvent { - // You can expand this into a richer participant struct later - std::vector participant_identities; + std::vector participants; }; struct E2eeStateChangedEvent { - std::string participant_identity; - EncryptionState state = EncryptionState::Unknown; + Participant *participant = nullptr; // local or remote, owned by room + EncryptionState state = EncryptionState::New; }; struct ChatMessageReceivedEvent { @@ -463,8 +479,10 @@ class RoomDelegate { virtual void onRoomEos(Room &, const RoomEosEvent &) {} // Data / transcription / chat - virtual void onDataPacketReceived(Room &, const DataPacketReceivedEvent &) {} - virtual void onTranscriptionReceived(Room &, const Transcription &) {} + virtual void onUserPacketReceived(Room &, const UserDataPacketEvent &) {} + virtual void onSipDtmfReceived(Room &, const SipDtmfReceivedEvent &) {} + virtual void onTranscriptionReceived(Room &, + const TranscriptionReceivedEvent &) {} virtual void onChatMessageReceived(Room &, const ChatMessageReceivedEvent &) { } diff --git a/include/livekit/track_publication.h b/include/livekit/track_publication.h index 19b8055..b0365bf 100644 --- a/include/livekit/track_publication.h +++ b/include/livekit/track_publication.h @@ -62,6 +62,7 @@ class TrackPublication { std::uint32_t height() const noexcept { return height_; } const std::string &mimeType() const noexcept { return mime_type_; } bool muted() const noexcept { return muted_; } + void setMuted(bool muted) noexcept { muted_ = muted; } EncryptionType encryptionType() const noexcept { return encryption_type_; } const std::vector &audioFeatures() const noexcept { @@ -74,7 +75,6 @@ class TrackPublication { /// Associated Track (if attached). std::shared_ptr track() const noexcept { return track_; } void setTrack(const std::shared_ptr &track) noexcept { - std::cout << "track_ is null " << (track_.get() == nullptr) << std::endl; track_ = track; } diff --git a/include/livekit/video_frame.h b/include/livekit/video_frame.h index 878aaca..987f696 100644 --- a/include/livekit/video_frame.h +++ b/include/livekit/video_frame.h @@ -61,6 +61,7 @@ class LKVideoFrame { LKVideoFrame(); LKVideoFrame(int width, int height, VideoBufferType type, std::vector data); + virtual ~LKVideoFrame() = default; LKVideoFrame(const LKVideoFrame &) = delete; LKVideoFrame &operator=(const LKVideoFrame &) = delete; diff --git a/include/livekit/video_source.h b/include/livekit/video_source.h index 95b6750..0e8c352 100644 --- a/include/livekit/video_source.h +++ b/include/livekit/video_source.h @@ -52,9 +52,7 @@ class VideoSource { * does not contain the expected new_video_source field. */ VideoSource(int width, int height); - - // Owned FFI handle will be released by FfiHandle's destructor. - ~VideoSource() = default; + virtual ~VideoSource() = default; VideoSource(const VideoSource &) = delete; VideoSource &operator=(const VideoSource &) = delete; diff --git a/include/livekit/video_stream.h b/include/livekit/video_stream.h index fea3a93..f3f233b 100644 --- a/include/livekit/video_stream.h +++ b/include/livekit/video_stream.h @@ -81,7 +81,7 @@ class VideoStream { TrackSource track_source, const Options &options); - ~VideoStream(); + virtual ~VideoStream(); VideoStream(const VideoStream &) = delete; VideoStream &operator=(const VideoStream &) = delete; diff --git a/src/audio_source.cpp b/src/audio_source.cpp index 36fb226..b8cebc9 100644 --- a/src/audio_source.cpp +++ b/src/audio_source.cpp @@ -56,11 +56,6 @@ AudioSource::AudioSource(int sample_rate, int num_channels, int queue_size_ms) handle_ = FfiHandle(static_cast(source_info.handle().id())); } -AudioSource::~AudioSource() { - // Let FfiHandle::~FfiHandle() drop the native handle. - // If you later add an explicit "dispose" request, you can send it here. -} - double AudioSource::queuedDuration() const noexcept { if (last_capture_ == 0.0) { return 0.0; diff --git a/src/local_participant.cpp b/src/local_participant.cpp index 02f90d5..d717024 100644 --- a/src/local_participant.cpp +++ b/src/local_participant.cpp @@ -350,4 +350,13 @@ void LocalParticipant::handleRpcMethodInvocation( FfiClient::instance().sendRequest(req); } +std::shared_ptr +LocalParticipant::findTrackPublication(const std::string &sid) const { + auto it = track_publications_.find(sid); + if (it == track_publications_.end()) { + return nullptr; + } + return std::static_pointer_cast(it->second); +} + } // namespace livekit diff --git a/src/remote_participant.cpp b/src/remote_participant.cpp index 38def8e..753f28f 100644 --- a/src/remote_participant.cpp +++ b/src/remote_participant.cpp @@ -45,4 +45,13 @@ std::ostream &operator<<(std::ostream &os, return os; } +std::shared_ptr +RemoteParticipant::findTrackPublication(const std::string &sid) const { + auto it = track_publications_.find(sid); + if (it == track_publications_.end()) { + return nullptr; + } + return std::static_pointer_cast(it->second); +} + } // namespace livekit diff --git a/src/room.cpp b/src/room.cpp index c20aac1..4f29583 100644 --- a/src/room.cpp +++ b/src/room.cpp @@ -45,7 +45,7 @@ using proto::FfiResponse; namespace { -std::unique_ptr +std::shared_ptr createRemoteParticipant(const proto::OwnedParticipant &owned) { const auto &pinfo = owned.info(); std::unordered_map attrs; @@ -56,7 +56,7 @@ createRemoteParticipant(const proto::OwnedParticipant &owned) { auto kind = livekit::fromProto(pinfo.kind()); auto reason = livekit::toDisconnectReason(pinfo.disconnect_reason()); livekit::FfiHandle handle(static_cast(owned.handle().id())); - return std::make_unique( + return std::make_shared( std::move(handle), pinfo.sid(), pinfo.name(), pinfo.identity(), pinfo.metadata(), std::move(attrs), kind, reason); } @@ -125,8 +125,8 @@ bool Room::Connect(const std::string &url, const std::string &token, for (const auto &owned_publication_info : pt.publications()) { auto publication = std::make_shared(owned_publication_info); - rp->mutable_track_publications().emplace(publication->sid(), - std::move(publication)); + rp->mutableTrackPublications().emplace(publication->sid(), + std::move(publication)); } remote_participants_.emplace(rp->identity(), std::move(rp)); @@ -210,27 +210,34 @@ void Room::OnEvent(const FfiEvent &event) { switch (re.message_case()) { case proto::RoomEvent::kParticipantConnected: { - auto ev = fromProto(re.participant_connected()); - std::cout << "kParticipantConnected " << std::endl; - // Create and register RemoteParticipant + std::shared_ptr new_participant; { std::lock_guard guard(lock_); - auto rp = createRemoteParticipant(re.participant_connected().info()); - remote_participants_.emplace(rp->identity(), std::move(rp)); + const auto &owned = re.participant_connected().info(); + // createRemoteParticipant takes proto::OwnedParticipant + new_participant = createRemoteParticipant(owned); + remote_participants_.emplace(new_participant->identity(), + new_participant); } - // TODO, use better public callback events + ParticipantConnectedEvent ev; + ev.participant = new_participant.get(); delegate_snapshot->onParticipantConnected(*this, ev); break; } case proto::RoomEvent::kParticipantDisconnected: { - auto ev = fromProto(re.participant_disconnected()); + std::shared_ptr removed; + DisconnectReason reason = DisconnectReason::Unknown; + { std::lock_guard guard(lock_); const auto &pd = re.participant_disconnected(); const std::string &identity = pd.participant_identity(); + reason = toDisconnectReason(pd.disconnect_reason()); + auto it = remote_participants_.find(identity); if (it != remote_participants_.end()) { + removed = it->second; remote_participants_.erase(it); } else { // We saw a disconnect event for a participant we don't track @@ -240,28 +247,86 @@ void Room::OnEvent(const FfiEvent &event) { << identity << std::endl; } } - // TODO, should we trigger onParticipantDisconnected if remote - // participants can't be found ? - delegate_snapshot->onParticipantDisconnected(*this, ev); + if (removed) { + ParticipantDisconnectedEvent ev; + ev.participant = removed.get(); + ev.reason = reason; + delegate_snapshot->onParticipantDisconnected(*this, ev); + } break; } case proto::RoomEvent::kLocalTrackPublished: { - auto ev = fromProto(re.local_track_published()); + LocalTrackPublishedEvent ev; + { + std::lock_guard guard(lock_); + if (!local_participant_) { + std::cerr << "kLocalTrackPublished: local_participant_ is nullptr" + << std::endl; + break; + } + const auto <p = re.local_track_published(); + const std::string &sid = ltp.track_sid(); + auto &pubs = local_participant_->trackPublications(); + auto it = pubs.find(sid); + if (it == pubs.end()) { + std::cerr << "local_track_published for unknown sid: " << sid + << std::endl; + break; + } + ev.publication = it->second; + ev.track = ev.publication ? ev.publication->track() : nullptr; + } delegate_snapshot->onLocalTrackPublished(*this, ev); break; } case proto::RoomEvent::kLocalTrackUnpublished: { - auto ev = fromProto(re.local_track_unpublished()); + LocalTrackUnpublishedEvent ev; + { + std::lock_guard guard(lock_); + if (!local_participant_) { + std::cerr << "kLocalTrackPublished: local_participant_ is nullptr" + << std::endl; + break; + } + const auto <u = re.local_track_unpublished(); + const std::string &pub_sid = ltu.publication_sid(); + auto &pubs = local_participant_->trackPublications(); + auto it = pubs.find(pub_sid); + if (it == pubs.end()) { + std::cerr << "local_track_unpublished for unknown publication sid: " + << pub_sid << std::endl; + break; + } + ev.publication = it->second; + } delegate_snapshot->onLocalTrackUnpublished(*this, ev); break; } case proto::RoomEvent::kLocalTrackSubscribed: { - auto ev = fromProto(re.local_track_subscribed()); + LocalTrackSubscribedEvent ev; + { + std::lock_guard guard(lock_); + if (!local_participant_) { + break; + } + const auto <s = re.local_track_subscribed(); + const std::string &sid = lts.track_sid(); + auto &pubs = local_participant_->trackPublications(); + auto it = pubs.find(sid); + if (it == pubs.end()) { + std::cerr << "local_track_subscribed for unknown sid: " << sid + << std::endl; + break; + } + auto publication = it->second; + ev.track = publication ? publication->track() : nullptr; + } + delegate_snapshot->onLocalTrackSubscribed(*this, ev); break; } case proto::RoomEvent::kTrackPublished: { - auto ev = fromProto(re.track_published()); + TrackPublishedEvent ev; { std::lock_guard guard(lock_); const auto &tp = re.track_published(); @@ -273,13 +338,14 @@ void Room::OnEvent(const FfiEvent &event) { auto rpublication = std::make_shared(owned_publication); // Store it on the participant, keyed by SID - rparticipant->mutable_track_publications().emplace( + rparticipant->mutableTrackPublications().emplace( rpublication->sid(), std::move(rpublication)); - + ev.participant = rparticipant; + ev.publication = rpublication; } else { // Optional: log if we get a track for an unknown participant std::cerr << "track_published for unknown participant: " << identity - << "\n"; + << std::endl; // Don't emit the break; } @@ -288,7 +354,31 @@ void Room::OnEvent(const FfiEvent &event) { break; } case proto::RoomEvent::kTrackUnpublished: { - auto ev = fromProto(re.track_unpublished()); + TrackUnpublishedEvent ev; + { + std::lock_guard guard(lock_); + const auto &tu = re.track_unpublished(); + const std::string &identity = tu.participant_identity(); + const std::string &pub_sid = tu.publication_sid(); + auto pit = remote_participants_.find(identity); + if (pit == remote_participants_.end()) { + std::cerr << "track_unpublished for unknown participant: " << identity + << std::endl; + break; + } + RemoteParticipant *rparticipant = pit->second.get(); + auto &pubs = rparticipant->mutableTrackPublications(); + auto it = pubs.find(pub_sid); + if (it == pubs.end()) { + std::cerr << "track_unpublished for unknown publication sid " + << pub_sid << " (participant " << identity << ")\n"; + break; + } + ev.participant = rparticipant; + ev.publication = it->second; + pubs.erase(it); + } + delegate_snapshot->onTrackUnpublished(*this, ev); break; } @@ -311,7 +401,7 @@ void Room::OnEvent(const FfiEvent &event) { } rparticipant = pit->second.get(); // Find existing publication by track SID (from track_published) - auto &pubs = rparticipant->mutable_track_publications(); + auto &pubs = rparticipant->mutableTrackPublications(); auto pubIt = pubs.find(track_info.sid()); if (pubIt == pubs.end()) { std::cerr << "track_subscribed for unknown publication sid " @@ -331,13 +421,9 @@ void Room::OnEvent(const FfiEvent &event) { << track_info.kind() << "\n"; break; } - std::cout << "before setTrack " << std::endl; - // Attach to publication, mark subscribed rpublication->setTrack(remote_track); - std::cout << "setTrack " << std::endl; rpublication->setSubscribed(true); - std::cout << "setSubscribed " << std::endl; } // Emit remote track_subscribed-style callback @@ -345,111 +431,478 @@ void Room::OnEvent(const FfiEvent &event) { ev.track = remote_track; ev.publication = rpublication; ev.participant = rparticipant; - std::cout << "onTrackSubscribed " << std::endl; delegate_snapshot->onTrackSubscribed(*this, ev); - std::cout << "after onTrackSubscribed " << std::endl; break; } case proto::RoomEvent::kTrackUnsubscribed: { - auto ev = fromProto(re.track_unsubscribed()); + TrackUnsubscribedEvent ev; + { + std::lock_guard guard(lock_); + const auto &tu = re.track_unsubscribed(); + const std::string &identity = tu.participant_identity(); + const std::string &track_sid = tu.track_sid(); + auto pit = remote_participants_.find(identity); + if (pit == remote_participants_.end()) { + std::cerr << "track_unsubscribed for unknown participant: " + << identity << "\n"; + break; + } + RemoteParticipant *rparticipant = pit->second.get(); + auto &pubs = rparticipant->mutableTrackPublications(); + auto pubIt = pubs.find(track_sid); + if (pubIt == pubs.end()) { + std::cerr << "track_unsubscribed for unknown publication sid " + << track_sid << " (participant " << identity << ")\n"; + break; + } + auto publication = pubIt->second; + auto track = publication->track(); + publication->setTrack(nullptr); + publication->setSubscribed(false); + ev.participant = rparticipant; + ev.publication = publication; + ev.track = track; + } + delegate_snapshot->onTrackUnsubscribed(*this, ev); break; } case proto::RoomEvent::kTrackSubscriptionFailed: { - auto ev = fromProto(re.track_subscription_failed()); + TrackSubscriptionFailedEvent ev; + { + std::lock_guard guard(lock_); + const auto &tsf = re.track_subscription_failed(); + const std::string &identity = tsf.participant_identity(); + auto pit = remote_participants_.find(identity); + if (pit == remote_participants_.end()) { + std::cerr << "track_subscription_failed for unknown participant: " + << identity << "\n"; + break; + } + ev.participant = pit->second.get(); + ev.track_sid = tsf.track_sid(); + ev.error = tsf.error(); + } delegate_snapshot->onTrackSubscriptionFailed(*this, ev); break; } case proto::RoomEvent::kTrackMuted: { - auto ev = fromProto(re.track_muted()); - delegate_snapshot->onTrackMuted(*this, ev); + TrackMutedEvent ev; + bool success = false; + { + std::lock_guard guard(lock_); + const auto &tm = re.track_muted(); + const std::string &identity = tm.participant_identity(); + const std::string &sid = tm.track_sid(); + Participant *participant = nullptr; + if (local_participant_ && local_participant_->identity() == identity) { + participant = local_participant_.get(); + } else { + auto pit = remote_participants_.find(identity); + if (pit != remote_participants_.end()) { + participant = pit->second.get(); + } + } + if (!participant) { + std::cerr << "track_muted for unknown participant: " << identity + << "\n"; + break; + } + auto pub = participant->findTrackPublication(sid); + if (!pub) { + std::cerr << "track_muted for unknown track sid: " << sid + << std::endl; + } else { + pub->setMuted(true); + if (auto t = pub->track()) { + t->setMuted(true); + } + ev.participant = participant; + ev.publication = pub; + success = true; + } + } + if (success) { + delegate_snapshot->onTrackMuted(*this, ev); + } break; } case proto::RoomEvent::kTrackUnmuted: { - auto ev = fromProto(re.track_unmuted()); - delegate_snapshot->onTrackUnmuted(*this, ev); + TrackUnmutedEvent ev; + bool success = false; + { + std::lock_guard guard(lock_); + const auto &tu = re.track_unmuted(); + const std::string &identity = tu.participant_identity(); + const std::string &sid = tu.track_sid(); + Participant *participant = nullptr; + if (local_participant_ && local_participant_->identity() == identity) { + participant = local_participant_.get(); + } else { + auto pit = remote_participants_.find(identity); + if (pit != remote_participants_.end()) { + participant = pit->second.get(); + } + } + if (!participant) { + std::cerr << "track_unmuted for unknown participant: " << identity + << "\n"; + break; + } + + auto pub = participant->findTrackPublication(sid); + if (!pub) { + std::cerr << "track_muted for unknown track sid: " << sid + << std::endl; + } else { + pub->setMuted(false); + if (auto t = pub->track()) { + t->setMuted(false); + } + ev.participant = participant; + ev.publication = pub; + success = true; + } + + ev.participant = participant; + ev.publication = pub; + } + + if (success) { + delegate_snapshot->onTrackUnmuted(*this, ev); + } break; } case proto::RoomEvent::kActiveSpeakersChanged: { - auto ev = fromProto(re.active_speakers_changed()); + ActiveSpeakersChangedEvent ev; + { + std::lock_guard guard(lock_); + const auto &asc = re.active_speakers_changed(); + for (const auto &identity : asc.participant_identities()) { + Participant *participant = nullptr; + if (local_participant_ && + local_participant_->identity() == identity) { + participant = local_participant_.get(); + } else { + auto pit = remote_participants_.find(identity); + if (pit != remote_participants_.end()) { + participant = pit->second.get(); + } + } + if (participant) { + ev.speakers.push_back(participant); + } + } + } delegate_snapshot->onActiveSpeakersChanged(*this, ev); break; } case proto::RoomEvent::kRoomMetadataChanged: { - auto ev = fromProto(re.room_metadata_changed()); + RoomMetadataChangedEvent ev; + { + std::lock_guard guard(lock_); + const auto old_metadata = room_info_.metadata; + room_info_.metadata = re.room_metadata_changed().metadata(); + ev.old_metadata = old_metadata; + ev.new_metadata = room_info_.metadata; + } delegate_snapshot->onRoomMetadataChanged(*this, ev); break; } case proto::RoomEvent::kRoomSidChanged: { - auto ev = fromProto(re.room_sid_changed()); + RoomSidChangedEvent ev; + { + std::lock_guard guard(lock_); + room_info_.sid = re.room_sid_changed().sid(); + ev.sid = room_info_.sid.value_or(std::string{}); + } delegate_snapshot->onRoomSidChanged(*this, ev); break; } case proto::RoomEvent::kParticipantMetadataChanged: { - auto ev = fromProto(re.participant_metadata_changed()); + ParticipantMetadataChangedEvent ev; + { + std::lock_guard guard(lock_); + const auto &pm = re.participant_metadata_changed(); + const std::string &identity = pm.participant_identity(); + Participant *participant = nullptr; + if (local_participant_ && local_participant_->identity() == identity) { + participant = local_participant_.get(); + } else { + auto it = remote_participants_.find(identity); + if (it != remote_participants_.end()) { + participant = it->second.get(); + } + } + if (!participant) { + std::cerr << "participant_metadata_changed for unknown participant: " + << identity << "\n"; + break; + } + std::string old_metadata = participant->metadata(); + participant->set_metadata(pm.metadata()); + ev.participant = participant; + ev.old_metadata = old_metadata; + ev.new_metadata = participant->metadata(); + } + delegate_snapshot->onParticipantMetadataChanged(*this, ev); break; } case proto::RoomEvent::kParticipantNameChanged: { - auto ev = fromProto(re.participant_name_changed()); + ParticipantNameChangedEvent ev; + { + std::lock_guard guard(lock_); + const auto &pn = re.participant_name_changed(); + const std::string &identity = pn.participant_identity(); + Participant *participant = nullptr; + if (local_participant_ && local_participant_->identity() == identity) { + participant = local_participant_.get(); + } else { + auto it = remote_participants_.find(identity); + if (it != remote_participants_.end()) { + participant = it->second.get(); + } + } + if (!participant) { + std::cerr << "participant_name_changed for unknown participant: " + << identity << "\n"; + break; + } + std::string old_name = participant->name(); + participant->set_name(pn.name()); + ev.participant = participant; + ev.old_name = old_name; + ev.new_name = participant->name(); + } delegate_snapshot->onParticipantNameChanged(*this, ev); break; } case proto::RoomEvent::kParticipantAttributesChanged: { - auto ev = fromProto(re.participant_attributes_changed()); + ParticipantAttributesChangedEvent ev; + { + std::lock_guard guard(lock_); + const auto &pa = re.participant_attributes_changed(); + const std::string &identity = pa.participant_identity(); + Participant *participant = nullptr; + if (local_participant_ && local_participant_->identity() == identity) { + participant = local_participant_.get(); + } else { + auto it = remote_participants_.find(identity); + if (it != remote_participants_.end()) { + participant = it->second.get(); + } + } + if (!participant) { + std::cerr + << "participant_attributes_changed for unknown participant: " + << identity << "\n"; + break; + } + // Build full attributes map + std::unordered_map attrs; + for (const auto &entry : pa.attributes()) { + attrs.emplace(entry.key(), entry.value()); + } + participant->set_attributes(attrs); + + // Build changed_attributes map + for (const auto &entry : pa.changed_attributes()) { + ev.changed_attributes.emplace_back(entry.key(), entry.value()); + } + ev.participant = participant; + } delegate_snapshot->onParticipantAttributesChanged(*this, ev); break; } case proto::RoomEvent::kParticipantEncryptionStatusChanged: { - auto ev = fromProto(re.participant_encryption_status_changed()); + ParticipantEncryptionStatusChangedEvent ev; + { + std::lock_guard guard(lock_); + const auto &pe = re.participant_encryption_status_changed(); + const std::string &identity = pe.participant_identity(); + Participant *participant = nullptr; + if (local_participant_ && local_participant_->identity() == identity) { + participant = local_participant_.get(); + } else { + auto it = remote_participants_.find(identity); + if (it != remote_participants_.end()) { + participant = it->second.get(); + } + } + if (!participant) { + std::cerr << "participant_encryption_status_changed for unknown " + "participant: " + << identity << "\n"; + break; + } + ev.participant = participant; + ev.is_encrypted = pe.is_encrypted(); + } + delegate_snapshot->onParticipantEncryptionStatusChanged(*this, ev); break; } case proto::RoomEvent::kConnectionQualityChanged: { - auto ev = fromProto(re.connection_quality_changed()); + ConnectionQualityChangedEvent ev; + { + std::lock_guard guard(lock_); + const auto &cq = re.connection_quality_changed(); + const std::string &identity = cq.participant_identity(); + Participant *participant = nullptr; + if (local_participant_ && local_participant_->identity() == identity) { + participant = local_participant_.get(); + } else { + auto it = remote_participants_.find(identity); + if (it != remote_participants_.end()) { + participant = it->second.get(); + } + } + if (!participant) { + std::cerr << "connection_quality_changed for unknown participant: " + << identity << "\n"; + break; + } + ev.participant = participant; + ev.quality = static_cast(cq.quality()); + } + delegate_snapshot->onConnectionQualityChanged(*this, ev); break; } + + // ------------------------------------------------------------------------ + // Transcription + // ------------------------------------------------------------------------ + + case proto::RoomEvent::kTranscriptionReceived: { + TranscriptionReceivedEvent ev; + { + std::lock_guard guard(lock_); + const auto &tr = re.transcription_received(); + for (const auto &s : tr.segments()) { + TranscriptionSegment seg; + seg.id = s.id(); + seg.text = s.text(); + seg.final = s.final(); + seg.start_time = s.start_time(); + seg.end_time = s.end_time(); + seg.language = s.language(); + ev.segments.push_back(std::move(seg)); + } + + Participant *participant = nullptr; + if (!tr.participant_identity().empty()) { + const std::string &identity = tr.participant_identity(); + if (local_participant_ && + local_participant_->identity() == identity) { + participant = local_participant_.get(); + } else { + auto it = remote_participants_.find(identity); + if (it != remote_participants_.end()) { + participant = it->second.get(); + } + } + } + ev.participant = participant; + ev.publication = participant->findTrackPublication(tr.track_sid()); + } + + delegate_snapshot->onTranscriptionReceived(*this, ev); + break; + } + + // ------------------------------------------------------------------------ + // Data packets: user vs SIP DTMF + // ------------------------------------------------------------------------ + case proto::RoomEvent::kDataPacketReceived: { + const auto &dp = re.data_packet_received(); + RemoteParticipant *rp = nullptr; + { + std::lock_guard guard(lock_); + auto it = remote_participants_.find(dp.participant_identity()); + if (it != remote_participants_.end()) { + rp = it->second.get(); + } + } + const auto which_val = dp.value_case(); + if (which_val == proto::DataPacketReceived::kUser) { + UserDataPacketEvent ev = userDataPacketFromProto(dp, rp); + delegate_snapshot->onUserPacketReceived(*this, ev); + } else if (which_val == proto::DataPacketReceived::kSipDtmf) { + SipDtmfReceivedEvent ev = sipDtmfFromProto(dp, rp); + delegate_snapshot->onSipDtmfReceived(*this, ev); + } + break; + } + + // ------------------------------------------------------------------------ + // E2EE state + // ------------------------------------------------------------------------ + case proto::RoomEvent::kE2EeStateChanged: { + E2eeStateChangedEvent ev; + { + std::lock_guard guard(lock_); + const auto &es = re.e2ee_state_changed(); + const std::string &identity = es.participant_identity(); + Participant *participant = nullptr; + if (local_participant_ && local_participant_->identity() == identity) { + participant = local_participant_.get(); + } else { + auto it = remote_participants_.find(identity); + if (it != remote_participants_.end()) { + participant = it->second.get(); + } + } + if (!participant) { + std::cerr << "e2ee_state_changed for unknown participant: " + << identity << std::endl; + break; + } + + ev.participant = participant; + ev.state = static_cast(es.state()); + } + delegate_snapshot->onE2eeStateChanged(*this, ev); + break; + } + + // ------------------------------------------------------------------------ + // Connection state / lifecycle + // ------------------------------------------------------------------------ + case proto::RoomEvent::kConnectionStateChanged: { - auto ev = fromProto(re.connection_state_changed()); + ConnectionStateChangedEvent ev; + { + std::lock_guard guard(lock_); + const auto &cs = re.connection_state_changed(); + connection_state_ = static_cast(cs.state()); + ev.state = connection_state_; + } delegate_snapshot->onConnectionStateChanged(*this, ev); break; } case proto::RoomEvent::kDisconnected: { - auto ev = fromProto(re.disconnected()); + DisconnectedEvent ev; + ev.reason = toDisconnectReason(re.disconnected().reason()); delegate_snapshot->onDisconnected(*this, ev); break; } case proto::RoomEvent::kReconnecting: { - auto ev = fromProto(re.reconnecting()); + ReconnectingEvent ev; delegate_snapshot->onReconnecting(*this, ev); break; } case proto::RoomEvent::kReconnected: { - auto ev = fromProto(re.reconnected()); + ReconnectedEvent ev; delegate_snapshot->onReconnected(*this, ev); break; } - case proto::RoomEvent::kE2EeStateChanged: { - auto ev = fromProto(re.e2ee_state_changed()); - delegate_snapshot->onE2eeStateChanged(*this, ev); - break; - } case proto::RoomEvent::kEos: { - auto ev = fromProto(re.eos()); + RoomEosEvent ev; delegate_snapshot->onRoomEos(*this, ev); break; } - case proto::RoomEvent::kDataPacketReceived: { - auto ev = fromProto(re.data_packet_received()); - delegate_snapshot->onDataPacketReceived(*this, ev); - break; - } - case proto::RoomEvent::kTranscriptionReceived: { - auto ev = fromProto(re.transcription_received()); - delegate_snapshot->onTranscriptionReceived(*this, ev); - break; - } case proto::RoomEvent::kChatMessage: { auto ev = fromProto(re.chat_message()); delegate_snapshot->onChatMessageReceived(*this, ev); @@ -497,36 +950,33 @@ void Room::OnEvent(const FfiEvent &event) { break; } case proto::RoomEvent::kParticipantsUpdated: { - auto ev = fromProto(re.participants_updated()); + ParticipantsUpdatedEvent ev; { std::lock_guard guard(lock_); const auto &pu = re.participants_updated(); for (const auto &info : pu.participants()) { const std::string &identity = info.identity(); Participant *participant = nullptr; - // First, check local participant. + if (local_participant_ && identity == local_participant_->identity()) { participant = local_participant_.get(); } else { - // Otherwise, look for a remote participant. auto it = remote_participants_.find(identity); if (it != remote_participants_.end()) { participant = it->second.get(); } } - if (!participant) { - // Participant might not exist yet; ignore for now. std::cerr << "Room::RoomEvent::kParticipantsUpdated participant " "does not exist: " << identity << std::endl; continue; } - // Update basic fields participant->set_name(info.name()); participant->set_metadata(info.metadata()); + std::unordered_map attrs; attrs.reserve(info.attributes_size()); for (const auto &kv : info.attributes()) { @@ -536,6 +986,8 @@ void Room::OnEvent(const FfiEvent &event) { participant->set_kind(fromProto(info.kind())); participant->set_disconnect_reason( toDisconnectReason(info.disconnect_reason())); + + ev.participants.push_back(participant); } } delegate_snapshot->onParticipantsUpdated(*this, ev); diff --git a/src/room_proto_converter.cpp b/src/room_proto_converter.cpp index 7a6b1c1..9173de6 100644 --- a/src/room_proto_converter.cpp +++ b/src/room_proto_converter.cpp @@ -62,11 +62,6 @@ DataPacketKind toDataPacketKind(proto::DataPacketKind in) { } } -EncryptionState toEncryptionState(proto::EncryptionState /*in*/) { - // TODO: fill out once you have the proto::EncryptionState enum - return EncryptionState::Unknown; -} - DisconnectReason toDisconnectReason(proto::DisconnectReason /*in*/) { // TODO: map each proto::DisconnectReason to your DisconnectReason enum return DisconnectReason::Unknown; @@ -216,174 +211,12 @@ DataStreamTrailerData fromProto(const proto::DataStream_Trailer &in) { // --------- event conversions --------- -ParticipantConnectedEvent fromProto(const proto::ParticipantConnected &in) { - ParticipantConnectedEvent ev; - const auto &pinfo = in.info().info(); - ev.identity = pinfo.identity(); - ev.name = pinfo.name(); - ev.metadata = pinfo.metadata(); - return ev; -} - -ParticipantDisconnectedEvent -fromProto(const proto::ParticipantDisconnected &in) { - ParticipantDisconnectedEvent ev; - ev.participant_identity = in.participant_identity(); - ev.reason = toDisconnectReason(in.disconnect_reason()); - return ev; -} - -LocalTrackPublishedEvent fromProto(const proto::LocalTrackPublished &in) { - LocalTrackPublishedEvent ev; - ev.track_sid = in.track_sid(); - return ev; -} - -LocalTrackUnpublishedEvent fromProto(const proto::LocalTrackUnpublished &in) { - LocalTrackUnpublishedEvent ev; - ev.publication_sid = in.publication_sid(); - return ev; -} - -LocalTrackSubscribedEvent fromProto(const proto::LocalTrackSubscribed &in) { - LocalTrackSubscribedEvent ev; - ev.track_sid = in.track_sid(); - return ev; -} - -TrackPublishedEvent fromProto(const proto::TrackPublished &in) { - TrackPublishedEvent ev; - ev.participant_identity = in.participant_identity(); - // OwnedTrackPublication publication = 2; - // TODO: map publication info once you inspect OwnedTrackPublication - // ev.publication_sid = in.publication().info().sid(); - // ev.track_name = in.publication().info().name(); - // ev.track_kind = ...; - // ev.track_source = ...; - return ev; -} - -TrackUnpublishedEvent fromProto(const proto::TrackUnpublished &in) { - TrackUnpublishedEvent ev; - ev.participant_identity = in.participant_identity(); - ev.publication_sid = in.publication_sid(); - return ev; -} - -TrackUnsubscribedEvent fromProto(const proto::TrackUnsubscribed &in) { - TrackUnsubscribedEvent ev; - ev.participant_identity = in.participant_identity(); - ev.track_sid = in.track_sid(); - return ev; -} - -TrackSubscriptionFailedEvent -fromProto(const proto::TrackSubscriptionFailed &in) { - TrackSubscriptionFailedEvent ev; - ev.participant_identity = in.participant_identity(); - ev.track_sid = in.track_sid(); - ev.error = in.error(); - return ev; -} - -TrackMutedEvent fromProto(const proto::TrackMuted &in) { - TrackMutedEvent ev; - ev.participant_identity = in.participant_identity(); - ev.track_sid = in.track_sid(); - return ev; -} - -TrackUnmutedEvent fromProto(const proto::TrackUnmuted &in) { - TrackUnmutedEvent ev; - ev.participant_identity = in.participant_identity(); - ev.track_sid = in.track_sid(); - return ev; -} - -ActiveSpeakersChangedEvent fromProto(const proto::ActiveSpeakersChanged &in) { - ActiveSpeakersChangedEvent ev; - for (const auto &id : in.participant_identities()) { - ev.participant_identities.push_back(id); - } - return ev; -} - -RoomMetadataChangedEvent fromProto(const proto::RoomMetadataChanged &in) { - RoomMetadataChangedEvent ev; - ev.metadata = in.metadata(); - return ev; -} - RoomSidChangedEvent fromProto(const proto::RoomSidChanged &in) { RoomSidChangedEvent ev; ev.sid = in.sid(); return ev; } -ParticipantMetadataChangedEvent -fromProto(const proto::ParticipantMetadataChanged &in) { - ParticipantMetadataChangedEvent ev; - ev.participant_identity = in.participant_identity(); - ev.metadata = in.metadata(); - return ev; -} - -ParticipantNameChangedEvent fromProto(const proto::ParticipantNameChanged &in) { - ParticipantNameChangedEvent ev; - ev.participant_identity = in.participant_identity(); - ev.name = in.name(); - return ev; -} - -ParticipantAttributesChangedEvent -fromProto(const proto::ParticipantAttributesChanged &in) { - ParticipantAttributesChangedEvent ev; - ev.participant_identity = in.participant_identity(); - for (const auto &a : in.attributes()) { - ev.attributes.push_back(fromProto(a)); - } - for (const auto &a : in.changed_attributes()) { - ev.changed_attributes.push_back(fromProto(a)); - } - return ev; -} - -ParticipantEncryptionStatusChangedEvent -fromProto(const proto::ParticipantEncryptionStatusChanged &in) { - ParticipantEncryptionStatusChangedEvent ev; - ev.participant_identity = in.participant_identity(); - ev.is_encrypted = in.is_encrypted(); - return ev; -} - -ConnectionQualityChangedEvent -fromProto(const proto::ConnectionQualityChanged &in) { - ConnectionQualityChangedEvent ev; - ev.participant_identity = in.participant_identity(); - ev.quality = toConnectionQuality(in.quality()); - return ev; -} - -DataPacketReceivedEvent fromProto(const proto::DataPacketReceived &in) { - DataPacketReceivedEvent ev; - ev.kind = toDataPacketKind(in.kind()); - ev.participant_identity = in.participant_identity(); - - switch (in.value_case()) { - case proto::DataPacketReceived::kUser: - ev.user = fromProto(in.user()); - break; - case proto::DataPacketReceived::kSipDtmf: - ev.sip_dtmf = fromProto(in.sip_dtmf()); - break; - case proto::DataPacketReceived::VALUE_NOT_SET: - default: - break; - } - - return ev; -} - ConnectionStateChangedEvent fromProto(const proto::ConnectionStateChanged &in) { ConnectionStateChangedEvent ev; ev.state = toConnectionState(in.state()); @@ -466,23 +299,6 @@ RoomMovedEvent roomMovedFromProto(const proto::RoomInfo &in) { return ev; } -ParticipantsUpdatedEvent fromProto(const proto::ParticipantsUpdated &in) { - ParticipantsUpdatedEvent ev; - // We only know that it has ParticipantInfo participants = 1; - // TODO: fill real identities once you inspect proto::ParticipantInfo - for (const auto &p : in.participants()) { - ev.participant_identities.push_back(p.identity()); - } - return ev; -} - -E2eeStateChangedEvent fromProto(const proto::E2eeStateChanged &in) { - E2eeStateChangedEvent ev; - ev.participant_identity = in.participant_identity(); - ev.state = toEncryptionState(in.state()); - return ev; -} - ChatMessageReceivedEvent fromProto(const proto::ChatMessageReceived &in) { ChatMessageReceivedEvent ev; ev.message = fromProto(in.message()); @@ -604,21 +420,6 @@ TranscriptionSegment fromProto(const proto::TranscriptionSegment &in) { return out; } -proto::TranscriptionReceived toProto(const Transcription &in) { - proto::TranscriptionReceived msg; - if (in.participant_identity) { - msg.set_participant_identity(*in.participant_identity); - } - if (in.track_sid) { - msg.set_track_sid(*in.track_sid); - } - for (const auto &seg : in.segments) { - auto *pseg = msg.add_segments(); - pseg->CopyFrom(toProto(seg)); - } - return msg; -} - Transcription fromProto(const proto::TranscriptionReceived &in) { Transcription out; if (in.has_participant_identity()) { @@ -634,4 +435,34 @@ Transcription fromProto(const proto::TranscriptionReceived &in) { return out; } +UserDataPacketEvent userDataPacketFromProto(const proto::DataPacketReceived &in, + RemoteParticipant *participant) { + UserDataPacketEvent ev; + ev.kind = static_cast(in.kind()); + ev.participant = participant; + ev.topic = in.user().topic(); + + // Copy bytes + const auto &owned = in.user().data(); + const auto &info = owned.data(); + if (info.data_ptr() != 0 && info.data_len() > 0) { + auto ptr = reinterpret_cast(info.data_ptr()); + auto len = static_cast(info.data_len()); + ev.data.assign(ptr, ptr + len); + } else { + ev.data.clear(); + } + + return ev; +} + +SipDtmfReceivedEvent sipDtmfFromProto(const proto::DataPacketReceived &in, + RemoteParticipant *participant) { + SipDtmfReceivedEvent ev; + ev.participant = participant; + ev.code = in.sip_dtmf().code(); + ev.digit = in.sip_dtmf().digit(); + return ev; +} + } // namespace livekit diff --git a/src/room_proto_converter.h b/src/room_proto_converter.h index 127a70c..8d71fd9 100644 --- a/src/room_proto_converter.h +++ b/src/room_proto_converter.h @@ -24,20 +24,19 @@ namespace livekit { enum class RpcErrorCode; +class RemoteParticipant; // --------- basic helper conversions --------- ConnectionQuality toConnectionQuality(proto::ConnectionQuality in); ConnectionState toConnectionState(proto::ConnectionState in); DataPacketKind toDataPacketKind(proto::DataPacketKind in); -EncryptionState toEncryptionState(proto::EncryptionState in); DisconnectReason toDisconnectReason(proto::DisconnectReason in); ChatMessageData fromProto(const proto::ChatMessage &in); UserPacketData fromProto(const proto::UserPacket &in); SipDtmfData fromProto(const proto::SipDTMF &in); RoomInfoData fromProto(const proto::RoomInfo &in); -AttributeEntry fromProto(const proto::AttributesEntry &in); DataStreamHeaderData fromProto(const proto::DataStream_Header &in); DataStreamChunkData fromProto(const proto::DataStream_Chunk &in); @@ -63,22 +62,11 @@ TrackUnmutedEvent fromProto(const proto::TrackUnmuted &in); ActiveSpeakersChangedEvent fromProto(const proto::ActiveSpeakersChanged &in); -RoomMetadataChangedEvent fromProto(const proto::RoomMetadataChanged &in); RoomSidChangedEvent fromProto(const proto::RoomSidChanged &in); -ParticipantMetadataChangedEvent -fromProto(const proto::ParticipantMetadataChanged &in); -ParticipantNameChangedEvent fromProto(const proto::ParticipantNameChanged &in); -ParticipantAttributesChangedEvent -fromProto(const proto::ParticipantAttributesChanged &in); -ParticipantEncryptionStatusChangedEvent -fromProto(const proto::ParticipantEncryptionStatusChanged &in); - ConnectionQualityChangedEvent fromProto(const proto::ConnectionQualityChanged &in); -DataPacketReceivedEvent fromProto(const proto::DataPacketReceived &in); - ConnectionStateChangedEvent fromProto(const proto::ConnectionStateChanged &in); DisconnectedEvent fromProto(const proto::Disconnected &in); ReconnectingEvent fromProto(const proto::Reconnecting &in); @@ -122,7 +110,15 @@ TrackPublishOptions fromProto(const proto::TrackPublishOptions &in); proto::TranscriptionSegment toProto(const TranscriptionSegment &in); TranscriptionSegment fromProto(const proto::TranscriptionSegment &in); -proto::TranscriptionReceived toProto(const Transcription &in); +proto::TranscriptionReceived toProto(const TranscriptionReceivedEvent &in); Transcription fromProto(const proto::TranscriptionReceived &in); +// --------- room Data Packet conversions --------- + +UserDataPacketEvent userDataPacketFromProto(const proto::DataPacketReceived &in, + RemoteParticipant *participant); + +SipDtmfReceivedEvent sipDtmfFromProto(const proto::DataPacketReceived &in, + RemoteParticipant *participant); + } // namespace livekit diff --git a/src/video_stream.cpp b/src/video_stream.cpp index 7847920..cb6d447 100644 --- a/src/video_stream.cpp +++ b/src/video_stream.cpp @@ -86,7 +86,6 @@ bool VideoStream::read(VideoFrameEvent &out) { } void VideoStream::close() { - std::cout << "VideoSream::close() \n"; { std::lock_guard lock(mutex_); if (closed_) { @@ -211,17 +210,14 @@ void VideoStream::pushFrame(VideoFrameEvent &&ev) { } void VideoStream::pushEos() { - std::cout << "pushEos 1" << std::endl; { std::lock_guard lock(mutex_); if (eof_) { - std::cout << "pushEos 2" << std::endl; return; } eof_ = true; } cv_.notify_all(); - std::cout << "pushEos 3" << std::endl; } } // namespace livekit From e08a9644d3c4d06097af883af674716fe96879b9 Mon Sep 17 00:00:00 2001 From: shijing xian Date: Mon, 8 Dec 2025 23:32:50 -0800 Subject: [PATCH 10/12] removed the un-needed functions from room_proto_converter.h --- src/room_proto_converter.h | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/src/room_proto_converter.h b/src/room_proto_converter.h index 8d71fd9..14b58f8 100644 --- a/src/room_proto_converter.h +++ b/src/room_proto_converter.h @@ -44,29 +44,8 @@ DataStreamTrailerData fromProto(const proto::DataStream_Trailer &in); // --------- event conversions (RoomEvent.oneof message) --------- -ParticipantConnectedEvent fromProto(const proto::ParticipantConnected &in); -ParticipantDisconnectedEvent -fromProto(const proto::ParticipantDisconnected &in); - -LocalTrackPublishedEvent fromProto(const proto::LocalTrackPublished &in); -LocalTrackUnpublishedEvent fromProto(const proto::LocalTrackUnpublished &in); -LocalTrackSubscribedEvent fromProto(const proto::LocalTrackSubscribed &in); - -TrackPublishedEvent fromProto(const proto::TrackPublished &in); -TrackUnpublishedEvent fromProto(const proto::TrackUnpublished &in); -TrackUnsubscribedEvent fromProto(const proto::TrackUnsubscribed &in); -TrackSubscriptionFailedEvent -fromProto(const proto::TrackSubscriptionFailed &in); -TrackMutedEvent fromProto(const proto::TrackMuted &in); -TrackUnmutedEvent fromProto(const proto::TrackUnmuted &in); - -ActiveSpeakersChangedEvent fromProto(const proto::ActiveSpeakersChanged &in); - RoomSidChangedEvent fromProto(const proto::RoomSidChanged &in); -ConnectionQualityChangedEvent -fromProto(const proto::ConnectionQualityChanged &in); - ConnectionStateChangedEvent fromProto(const proto::ConnectionStateChanged &in); DisconnectedEvent fromProto(const proto::Disconnected &in); ReconnectingEvent fromProto(const proto::Reconnecting &in); @@ -90,8 +69,6 @@ RoomUpdatedEvent roomUpdatedFromProto(const proto::RoomInfo &in); // room_updated RoomMovedEvent roomMovedFromProto(const proto::RoomInfo &in); // moved -ParticipantsUpdatedEvent fromProto(const proto::ParticipantsUpdated &in); -E2eeStateChangedEvent fromProto(const proto::E2eeStateChanged &in); ChatMessageReceivedEvent fromProto(const proto::ChatMessageReceived &in); // --------- room options conversions --------- From 5061187d1c46cb62756c4951da9ac9b8d598fcdb Mon Sep 17 00:00:00 2001 From: shijing xian Date: Wed, 10 Dec 2025 11:23:33 -0800 Subject: [PATCH 11/12] move the room event types into room_event_types.h from room_delegate.h, and fix the comments --- CMakeLists.txt | 1 + include/livekit/livekit.h | 1 + include/livekit/local_participant.h | 2 +- include/livekit/room.h | 2 +- include/livekit/room_delegate.h | 584 +++++++------------- include/livekit/room_event_types.h | 808 ++++++++++++++++++++++++++++ src/remote_participant.cpp | 2 + src/room.cpp | 5 +- src/room_proto_converter.h | 2 +- 9 files changed, 1010 insertions(+), 397 deletions(-) create mode 100644 include/livekit/room_event_types.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 473fd49..ec04b46 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -160,6 +160,7 @@ add_library(livekit include/livekit/audio_source.h include/livekit/audio_stream.h include/livekit/room.h + include/livekit/room_event_types.h include/livekit/room_delegate.h include/livekit/ffi_handle.h include/livekit/ffi_client.h diff --git a/include/livekit/livekit.h b/include/livekit/livekit.h index 3eab63d..6bb4152 100644 --- a/include/livekit/livekit.h +++ b/include/livekit/livekit.h @@ -26,6 +26,7 @@ #include "remote_track_publication.h" #include "room.h" #include "room_delegate.h" +#include "room_event_types.h" #include "track_publication.h" #include "video_frame.h" #include "video_source.h" diff --git a/include/livekit/local_participant.h b/include/livekit/local_participant.h index 707da54..3d9e36a 100644 --- a/include/livekit/local_participant.h +++ b/include/livekit/local_participant.h @@ -18,7 +18,7 @@ #include "livekit/ffi_handle.h" #include "livekit/participant.h" -#include "livekit/room_delegate.h" +#include "livekit/room_event_types.h" #include "livekit/rpc_error.h" #include diff --git a/include/livekit/room.h b/include/livekit/room.h index d244dd7..27e6654 100644 --- a/include/livekit/room.h +++ b/include/livekit/room.h @@ -19,7 +19,7 @@ #include "livekit/ffi_client.h" #include "livekit/ffi_handle.h" -#include "livekit/room_delegate.h" +#include "livekit/room_event_types.h" #include #include diff --git a/include/livekit/room_delegate.h b/include/livekit/room_delegate.h index 7237ea6..51b6c40 100644 --- a/include/livekit/room_delegate.h +++ b/include/livekit/room_delegate.h @@ -16,492 +16,296 @@ #pragma once -#include -#include -#include -#include -#include -#include - -#include "livekit/local_track_publication.h" -#include "livekit/remote_track_publication.h" -#include "livekit/track_publication.h" +#include "livekit/room_event_types.h" namespace livekit { class Room; -enum class VideoCodec; -enum class TrackSource; -class Track; -class RemoteParticipant; -class LocalTrackPublication; -class Participant; - -enum class ConnectionQuality { - Poor = 0, - Good, - Excellent, - Lost, -}; - -enum class ConnectionState { - Disconnected = 0, - Connected, - Reconnecting, -}; - -enum class DataPacketKind { - Lossy, - Reliable, -}; - -enum class EncryptionState { - New = 0, - Ok, - EncryptionFailed, - DecryptionFailed, - MissingKey, - KeyRatcheted, - InternalError, -}; - -enum class DisconnectReason { - Unknown = 0, - ClientInitiated, - DuplicateIdentity, - ServerShutdown, - ParticipantRemoved, - RoomDeleted, - StateMismatch, - JoinFailure, - Migration, - SignalClose, - RoomClosed, - UserUnavailable, - UserRejected, - SipTrunkFailure, - ConnectionTimeout, - MediaFailure -}; - -struct ChatMessageData { - std::string id; - std::int64_t timestamp = 0; - std::string message; - std::optional edit_timestamp; - bool deleted = false; - bool generated = false; -}; - -struct UserPacketData { - std::vector data; - std::optional topic; // optional -}; - -struct SipDtmfData { - std::uint32_t code = 0; - std::optional digit; -}; - -struct RoomInfoData { - std::optional sid; - std::string name; - std::string metadata; - std::uint64_t lossy_dc_buffered_amount_low_threshold = 0; - std::uint64_t reliable_dc_buffered_amount_low_threshold = 0; - std::uint32_t empty_timeout = 0; - std::uint32_t departure_timeout = 0; - std::uint32_t max_participants = 0; - std::int64_t creation_time = 0; - std::uint32_t num_participants = 0; - std::uint32_t num_publishers = 0; - bool active_recording = false; -}; - -struct AttributeEntry { - std::string key; - std::string value; - AttributeEntry() = default; - AttributeEntry(std::string k, std::string v) - : key(std::move(k)), value(std::move(v)) {} -}; - -struct DataStreamHeaderData { - std::string stream_id; - std::int64_t timestamp = 0; - std::string mime_type; - std::string topic; - std::optional total_length; - std::map attributes; - - // For content_header - enum class ContentType { - None, - Text, - Byte, - } content_type = ContentType::None; - - // TextHeader fields - enum class OperationType { - Create = 0, - Update = 1, - Delete = 2, - Reaction = 3, - }; - std::optional operation_type; - std::optional version; - std::optional reply_to_stream_id; - std::vector attached_stream_ids; - std::optional generated; - - // ByteHeader fields - std::optional name; -}; - -struct DataStreamChunkData { - std::string stream_id; - std::uint64_t chunk_index = 0; - std::vector content; - std::optional version; - std::vector iv; -}; - -struct DataStreamTrailerData { - std::string stream_id; - std::string reason; - std::map attributes; -}; - -// ------------- rooom.proto options ------------------------ - -struct VideoEncodingOptions { - std::uint64_t max_bitrate = 0; - double max_framerate = 0.0; -}; - -struct AudioEncodingOptions { - std::uint64_t max_bitrate = 0; -}; - -struct TrackPublishOptions { - std::optional video_encoding; - std::optional audio_encoding; - std::optional video_codec; - std::optional dtx; - std::optional red; - std::optional simulcast; - std::optional source; - std::optional stream; - std::optional preconnect_buffer; -}; - -// ------------- rooom.proto Transcription ------------------------ - -struct TranscriptionSegment { - std::string id; - std::string text; - std::uint64_t start_time = 0; - std::uint64_t end_time = 0; - bool final = false; - std::string language; -}; - -// --------------------------------------------------------- -// Event structs – β€œpublic” representations of RoomEvent.* -// --------------------------------------------------------- - -struct ParticipantConnectedEvent { - RemoteParticipant *participant = nullptr; // Owned by room -}; - -struct ParticipantDisconnectedEvent { - RemoteParticipant *participant = nullptr; // Owned by room - DisconnectReason reason = DisconnectReason::Unknown; -}; - -struct LocalTrackPublishedEvent { - std::shared_ptr publication; - std::shared_ptr track; -}; - -struct LocalTrackUnpublishedEvent { - std::shared_ptr publication; -}; - -struct LocalTrackSubscribedEvent { - std::shared_ptr track; -}; - -struct TrackPublishedEvent { - std::shared_ptr publication; - RemoteParticipant *participant = nullptr; // Owned by room -}; - -struct TrackUnpublishedEvent { - std::shared_ptr publication; - RemoteParticipant *participant = nullptr; -}; - -struct TrackSubscribedEvent { - std::shared_ptr track; - std::shared_ptr publication; - RemoteParticipant *participant = nullptr; // Owned by room -}; - -struct TrackUnsubscribedEvent { - std::shared_ptr track; - std::shared_ptr publication; - RemoteParticipant *participant = nullptr; // Owned by room -}; - -struct TrackSubscriptionFailedEvent { - RemoteParticipant *participant = nullptr; // Owned by room - std::string track_sid; - std::string error; -}; - -struct TrackMutedEvent { - Participant *participant = nullptr; // Local or Remote, owned by room - std::shared_ptr publication; -}; - -struct TrackUnmutedEvent { - Participant *participant = nullptr; // Local or Remote, owned by room - std::shared_ptr publication; -}; - -struct ActiveSpeakersChangedEvent { - std::vector speakers; -}; - -struct RoomMetadataChangedEvent { - std::string old_metadata; - std::string new_metadata; -}; - -struct RoomSidChangedEvent { - std::string sid; -}; - -struct ParticipantMetadataChangedEvent { - Participant *participant = nullptr; // Local or Remote, owned by room - std::string old_metadata; - std::string new_metadata; -}; - -struct ParticipantNameChangedEvent { - Participant *participant = nullptr; // Local or Remote, owned by room - std::string old_name; - std::string new_name; -}; - -struct ParticipantAttributesChangedEvent { - Participant *participant = nullptr; // Local or Remote, owned by room - std::vector changed_attributes; -}; - -struct ParticipantEncryptionStatusChangedEvent { - Participant *participant = nullptr; // Local or Remote, owned by room - bool is_encrypted = false; -}; - -struct ConnectionQualityChangedEvent { - Participant *participant = nullptr; // Local or Remote, owned by room - ConnectionQuality quality = ConnectionQuality::Good; -}; - -struct UserDataPacketEvent { - std::vector data; - DataPacketKind kind = DataPacketKind::Reliable; - RemoteParticipant *participant = nullptr; // may be null, owned by room - std::string topic; -}; - -struct SipDtmfReceivedEvent { - int code = 0; - std::string digit; - RemoteParticipant *participant = nullptr; // owned by room -}; - -struct Transcription { - std::optional participant_identity; - std::optional track_sid; - std::vector segments; -}; - -struct TranscriptionReceivedEvent { - std::vector segments; - Participant *participant = nullptr; // Local or Remote, owned by room - std::shared_ptr publication; -}; - -struct ConnectionStateChangedEvent { - ConnectionState state = ConnectionState::Disconnected; -}; - -struct DisconnectedEvent { - DisconnectReason reason = DisconnectReason::Unknown; -}; - -struct ReconnectingEvent {}; -struct ReconnectedEvent {}; - -struct RoomEosEvent {}; - -struct DataStreamHeaderReceivedEvent { - std::string participant_identity; - DataStreamHeaderData header; -}; - -struct DataStreamChunkReceivedEvent { - std::string participant_identity; - DataStreamChunkData chunk; -}; - -struct DataStreamTrailerReceivedEvent { - std::string participant_identity; - DataStreamTrailerData trailer; -}; - -struct DataChannelBufferedAmountLowThresholdChangedEvent { - DataPacketKind kind = DataPacketKind::Reliable; - std::uint64_t threshold = 0; -}; - -struct ByteStreamOpenedEvent { - std::uint64_t reader_handle = 0; // from OwnedByteStreamReader.handle - std::string participant_identity; -}; - -struct TextStreamOpenedEvent { - std::uint64_t reader_handle = 0; // from OwnedTextStreamReader.handle - std::string participant_identity; -}; - -struct RoomUpdatedEvent { - RoomInfoData info; -}; - -struct RoomMovedEvent { - RoomInfoData info; -}; - -struct ParticipantsUpdatedEvent { - std::vector participants; -}; - -struct E2eeStateChangedEvent { - Participant *participant = nullptr; // local or remote, owned by room - EncryptionState state = EncryptionState::New; -}; - -struct ChatMessageReceivedEvent { - ChatMessageData message; - std::string participant_identity; -}; - -// --------------------------------------------------------- -// RoomDelegate interface – NO protobuf dependency -// --------------------------------------------------------- +/** + * Interface for receiving room-level events. + * + * Implement this class and pass an instance to Room::setDelegate() + * to be notified about participants, tracks, data, and connection changes. + * + * All methods provide default no-op implementations so you can override + * only the callbacks you care about. + */ class RoomDelegate { public: virtual ~RoomDelegate() = default; - // Optional: generic hook with no payload - virtual void onRoomEvent(Room & /*room*/) {} - - // Per-event callbacks. All default no-op so you can add more later - // without breaking existing user code. - + // ------------------------------------------------------------------ // Participant lifecycle + // ------------------------------------------------------------------ + + /** + * Called when a new remote participant joins the room. + */ virtual void onParticipantConnected(Room &, const ParticipantConnectedEvent &) {} + + /** + * Called when a remote participant leaves the room. + */ virtual void onParticipantDisconnected(Room &, const ParticipantDisconnectedEvent &) { } - // Local track publication + // ------------------------------------------------------------------ + // Local track publication events + // ------------------------------------------------------------------ + + /** + * Called when a local track is successfully published. + */ virtual void onLocalTrackPublished(Room &, const LocalTrackPublishedEvent &) { } + + /** + * Called when a local track is unpublished. + */ virtual void onLocalTrackUnpublished(Room &, const LocalTrackUnpublishedEvent &) {} + + /** + * Called when a local track gains its first subscriber. + */ virtual void onLocalTrackSubscribed(Room &, const LocalTrackSubscribedEvent &) {} + // ------------------------------------------------------------------ // Remote track publication/subscription + // ------------------------------------------------------------------ + + /** + * Called when a remote participant publishes a track. + */ virtual void onTrackPublished(Room &, const TrackPublishedEvent &) {} + + /** + * Called when a remote participant unpublishes a track. + */ virtual void onTrackUnpublished(Room &, const TrackUnpublishedEvent &) {} + + /** + * Called when a remote track is successfully subscribed. + */ virtual void onTrackSubscribed(Room &, const TrackSubscribedEvent &) {} + + /** + * Called when a remote track is unsubscribed. + */ virtual void onTrackUnsubscribed(Room &, const TrackUnsubscribedEvent &) {} + + /** + * Called when subscribing to a remote track fails. + */ virtual void onTrackSubscriptionFailed(Room &, const TrackSubscriptionFailedEvent &) { } + + /** + * Called when a track is muted. + */ virtual void onTrackMuted(Room &, const TrackMutedEvent &) {} + + /** + * Called when a track is unmuted. + */ virtual void onTrackUnmuted(Room &, const TrackUnmutedEvent &) {} + // ------------------------------------------------------------------ // Active speakers + // ------------------------------------------------------------------ + + /** + * Called when the list of active speakers changes. + */ virtual void onActiveSpeakersChanged(Room &, const ActiveSpeakersChangedEvent &) {} + // ------------------------------------------------------------------ // Room info / metadata + // ------------------------------------------------------------------ + + /** + * Called when the room's metadata changes. + */ virtual void onRoomMetadataChanged(Room &, const RoomMetadataChangedEvent &) { } + + /** + * Called when the room SID changes (e.g., after migration). + */ virtual void onRoomSidChanged(Room &, const RoomSidChangedEvent &) {} + + /** + * Called when any room info is updated. + */ virtual void onRoomUpdated(Room &, const RoomUpdatedEvent &) {} + + /** + * Called when the participant is moved to another room. + */ virtual void onRoomMoved(Room &, const RoomMovedEvent &) {} + // ------------------------------------------------------------------ // Participant info changes + // ------------------------------------------------------------------ + + /** + * Called when a participant's metadata is updated. + */ virtual void onParticipantMetadataChanged(Room &, const ParticipantMetadataChangedEvent &) {} + + /** + * Called when a participant's name is changed. + */ virtual void onParticipantNameChanged(Room &, const ParticipantNameChangedEvent &) {} + + /** + * Called when a participant's attributes are updated. + */ virtual void onParticipantAttributesChanged(Room &, const ParticipantAttributesChangedEvent &) {} + + /** + * Called when a participant's encryption status changes. + */ virtual void onParticipantEncryptionStatusChanged( Room &, const ParticipantEncryptionStatusChangedEvent &) {} + // ------------------------------------------------------------------ // Connection quality / state + // ------------------------------------------------------------------ + + /** + * Called when a participant's connection quality changes. + */ virtual void onConnectionQualityChanged(Room &, const ConnectionQualityChangedEvent &) {} + + /** + * Called when the room's connection state changes. + */ virtual void onConnectionStateChanged(Room &, const ConnectionStateChangedEvent &) {} + + /** + * Called when the room is disconnected. + */ virtual void onDisconnected(Room &, const DisconnectedEvent &) {} + + /** + * Called before the SDK attempts to reconnect. + */ virtual void onReconnecting(Room &, const ReconnectingEvent &) {} + + /** + * Called after the SDK successfully reconnects. + */ virtual void onReconnected(Room &, const ReconnectedEvent &) {} + // ------------------------------------------------------------------ // E2EE + // ------------------------------------------------------------------ + + /** + * Called when a participant's end-to-end encryption state changes. + */ virtual void onE2eeStateChanged(Room &, const E2eeStateChangedEvent &) {} + // ------------------------------------------------------------------ // EOS + // ------------------------------------------------------------------ + + /** + * Called when the room reaches end-of-stream and will not emit further + * events. + */ virtual void onRoomEos(Room &, const RoomEosEvent &) {} + // ------------------------------------------------------------------ // Data / transcription / chat + // ------------------------------------------------------------------ + + /** + * Called when a user data packet (non-SIP) is received. + */ virtual void onUserPacketReceived(Room &, const UserDataPacketEvent &) {} + + /** + * Called when a SIP DTMF packet is received. + */ virtual void onSipDtmfReceived(Room &, const SipDtmfReceivedEvent &) {} + + /** + * Called when a transcription result is received. + */ virtual void onTranscriptionReceived(Room &, const TranscriptionReceivedEvent &) {} + + /** + * Called when a chat message is received. + */ virtual void onChatMessageReceived(Room &, const ChatMessageReceivedEvent &) { } + // ------------------------------------------------------------------ // Data streams + // ------------------------------------------------------------------ + + /** + * Called when a data stream header is received. + */ virtual void onDataStreamHeaderReceived(Room &, const DataStreamHeaderReceivedEvent &) {} + + /** + * Called when a data stream chunk is received. + */ virtual void onDataStreamChunkReceived(Room &, const DataStreamChunkReceivedEvent &) { } + + /** + * Called when a data stream trailer is received. + */ virtual void onDataStreamTrailerReceived(Room &, const DataStreamTrailerReceivedEvent &) {} + + /** + * Called when a data channel's buffered amount falls below its low threshold. + */ virtual void onDataChannelBufferedAmountLowThresholdChanged( Room &, const DataChannelBufferedAmountLowThresholdChangedEvent &) {} + // ------------------------------------------------------------------ // High-level byte/text streams + // ------------------------------------------------------------------ + + /** + * Called when a high-level byte stream reader is opened. + */ virtual void onByteStreamOpened(Room &, const ByteStreamOpenedEvent &) {} + + /** + * Called when a high-level text stream reader is opened. + */ virtual void onTextStreamOpened(Room &, const TextStreamOpenedEvent &) {} + // ------------------------------------------------------------------ // Participants snapshot + // ------------------------------------------------------------------ + + /** + * Called when a snapshot of participants has been updated. + */ virtual void onParticipantsUpdated(Room &, const ParticipantsUpdatedEvent &) { } }; diff --git a/include/livekit/room_event_types.h b/include/livekit/room_event_types.h new file mode 100644 index 0000000..e448245 --- /dev/null +++ b/include/livekit/room_event_types.h @@ -0,0 +1,808 @@ +/* + * Copyright 2025 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 +#include +#include +#include +#include +#include + +namespace livekit { + +// Forward declarations to avoid pulling in heavy headers. +class Track; +class Participant; +class RemoteParticipant; +class LocalTrackPublication; +class RemoteTrackPublication; +class TrackPublication; + +enum class VideoCodec; +enum class TrackSource; + +/** + * Overall quality of a participant's connection. + */ +enum class ConnectionQuality { + Poor = 0, + Good, + Excellent, + Lost, +}; + +/** + * Current connection state of the room. + */ +enum class ConnectionState { + Disconnected = 0, + Connected, + Reconnecting, +}; + +/** + * Type of data packet delivery semantics. + * + * - Lossy: unordered, unreliable (e.g. for real-time updates). + * - Reliable: ordered, reliable (e.g. for critical messages). + */ +enum class DataPacketKind { + Lossy, + Reliable, +}; + +/** + * End-to-end encryption state for a participant. + * + * These values mirror the proto::EncryptionState enum. + */ +enum class EncryptionState { + New = 0, + Ok, + EncryptionFailed, + DecryptionFailed, + MissingKey, + KeyRatcheted, + InternalError, +}; + +/** + * Reason why a participant or room was disconnected. + * + * These values mirror the server-side DisconnectReason enum. + */ +enum class DisconnectReason { + Unknown = 0, + ClientInitiated, + DuplicateIdentity, + ServerShutdown, + ParticipantRemoved, + RoomDeleted, + StateMismatch, + JoinFailure, + Migration, + SignalClose, + RoomClosed, + UserUnavailable, + UserRejected, + SipTrunkFailure, + ConnectionTimeout, + MediaFailure +}; + +/** + * A chat message associated with the room. + */ +struct ChatMessageData { + /** Unique ID of the message. */ + std::string id; + + /** Timestamp (ms since Unix epoch). */ + std::int64_t timestamp = 0; + + /** Message body. */ + std::string message; + + /** Optional timestamp when the message was edited (ms since Unix epoch). */ + std::optional edit_timestamp; + + /** True if the message has been deleted. */ + bool deleted = false; + + /** True if the message was generated (e.g. by an AI or system). */ + bool generated = false; +}; + +/** + * Application-level user data carried in a data packet. + */ +struct UserPacketData { + /** Raw payload bytes. */ + std::vector data; + + /** Optional topic name associated with this payload. */ + std::optional topic; +}; + +/** + * SIP DTMF payload carried via data packets. + */ +struct SipDtmfData { + /** DTMF code value. */ + std::uint32_t code = 0; + + /** Human-readable digit representation (e.g. "1", "#"). */ + std::optional digit; +}; + +/** + * Snapshot of core room information. + */ +struct RoomInfoData { + /** Room SID, if known. */ + std::optional sid; + + /** Room name. */ + std::string name; + + /** Arbitrary application metadata associated with the room. */ + std::string metadata; + + /** Low-watermark threshold for lossy data channel buffer. */ + std::uint64_t lossy_dc_buffered_amount_low_threshold = 0; + + /** Low-watermark threshold for reliable data channel buffer. */ + std::uint64_t reliable_dc_buffered_amount_low_threshold = 0; + + /** Time (seconds) to keep room open if no participants join. */ + std::uint32_t empty_timeout = 0; + + /** Time (seconds) to keep room open after last standard participant leaves. + */ + std::uint32_t departure_timeout = 0; + + /** Maximum number of participants allowed in the room. */ + std::uint32_t max_participants = 0; + + /** Creation time of the room (ms since Unix epoch). */ + std::int64_t creation_time = 0; + + /** Approximate number of participants (eventually consistent). */ + std::uint32_t num_participants = 0; + + /** Approximate number of publishers (eventually consistent). */ + std::uint32_t num_publishers = 0; + + /** True if the room is currently being recorded. */ + bool active_recording = false; +}; + +/** + * Key/value pair for participant or room attributes. + */ +struct AttributeEntry { + /** Attribute key. */ + std::string key; + + /** Attribute value. */ + std::string value; + + AttributeEntry() = default; + + AttributeEntry(std::string k, std::string v) + : key(std::move(k)), value(std::move(v)) {} +}; + +/** + * Header information for an incoming data stream. + * Represents proto_room.DataStream.Header in a C++-friendly form. + */ +struct DataStreamHeaderData { + /** Unique stream identifier. */ + std::string stream_id; + + /** Timestamp (ms since Unix epoch). */ + std::int64_t timestamp = 0; + + /** MIME type of the content (e.g. "application/json"). */ + std::string mime_type; + + /** Application-defined topic name. */ + std::string topic; + + /** Optional total length in bytes, if known. */ + std::optional total_length; + + /** Custom attributes associated with this stream. */ + std::map attributes; + + /** + * Content type carried by this stream. + */ + enum class ContentType { + None, + Text, + Byte, + } content_type = ContentType::None; + + /** + * Operation type for text streams. + */ + enum class OperationType { + Create = 0, + Update = 1, + Delete = 2, + Reaction = 3, + }; + + /** Optional operation type, for text content. */ + std::optional operation_type; + + /** Optional version number for the text stream. */ + std::optional version; + + /** Optional ID of the stream this one replies to. */ + std::optional reply_to_stream_id; + + /** IDs of streams attached to this one. */ + std::vector attached_stream_ids; + + /** True if this stream was generated (e.g. by AI). */ + std::optional generated; + + /** Optional filename for byte streams. */ + std::optional name; +}; + +/** + * One chunk of a data stream’s payload. + */ +struct DataStreamChunkData { + /** Stream identifier this chunk belongs to. */ + std::string stream_id; + + /** Zero-based index of this chunk. */ + std::uint64_t chunk_index = 0; + + /** Raw chunk content. */ + std::vector content; + + /** Optional version, mirroring header version if applicable. */ + std::optional version; + + /** Optional initialization vector for encrypted payloads. */ + std::vector iv; +}; + +/** + * Trailer metadata for a data stream, sent after all chunks. + */ +struct DataStreamTrailerData { + /** Stream identifier. */ + std::string stream_id; + + /** Reason why the stream ended (empty if normal completion). */ + std::string reason; + + /** Additional attributes describing the final state of the stream. */ + std::map attributes; +}; + +/** + * Video encoding configuration used when publishing a track. + */ +struct VideoEncodingOptions { + /** Maximum target bitrate in bps. */ + std::uint64_t max_bitrate = 0; + + /** Maximum frame rate in frames per second. */ + double max_framerate = 0.0; +}; + +/** + * Audio encoding configuration used when publishing a track. + */ +struct AudioEncodingOptions { + /** Maximum target bitrate in bps. */ + std::uint64_t max_bitrate = 0; +}; + +/** + * Options for publishing a track to the room. + */ +struct TrackPublishOptions { + /** Optional video encoding parameters. */ + std::optional video_encoding; + + /** Optional audio encoding parameters. */ + std::optional audio_encoding; + + /** Optional video codec to use. */ + std::optional video_codec; + + /** Enable or disable discontinuous transmission (DTX). */ + std::optional dtx; + + /** Enable or disable RED (redundant encoding). */ + std::optional red; + + /** Enable or disable simulcast. */ + std::optional simulcast; + + /** Track source (camera, microphone, screen share, etc.). */ + std::optional source; + + /** Optional stream label/group for this track. */ + std::optional stream; + + /** Enable pre-connect buffering for lower startup latency. */ + std::optional preconnect_buffer; +}; + +/** + * One transcription segment produced by speech recognition. + */ +struct TranscriptionSegment { + /** Segment identifier. */ + std::string id; + + /** Transcribed text. */ + std::string text; + + /** Start time (ms) relative to the beginning of the audio source. */ + std::uint64_t start_time = 0; + + /** End time (ms) relative to the beginning of the audio source. */ + std::uint64_t end_time = 0; + + /** True if this segment is final and will not be updated further. */ + bool final = false; + + /** Language code (e.g. "en-US"). */ + std::string language; +}; + +// --------------------------------------------------------- +// Event structs – public representations of RoomEvent.* +// --------------------------------------------------------- + +/** + * Fired when a remote participant joins the room. + */ +struct ParticipantConnectedEvent { + /** The newly connected remote participant (owned by Room). */ + RemoteParticipant *participant = nullptr; +}; + +/** + * Fired when a remote participant leaves the room. + */ +struct ParticipantDisconnectedEvent { + /** The participant that disconnected (owned by Room). */ + RemoteParticipant *participant = nullptr; + + /** Reason for the disconnect, if known. */ + DisconnectReason reason = DisconnectReason::Unknown; +}; + +/** + * Fired when a local track is successfully published. + */ +struct LocalTrackPublishedEvent { + /** Track publication for the local track. */ + std::shared_ptr publication; + + /** The published local track. */ + std::shared_ptr track; +}; + +/** + * Fired when a local track is unpublished. + */ +struct LocalTrackUnpublishedEvent { + /** Publication that was unpublished. */ + std::shared_ptr publication; +}; + +/** + * Fired when a local track gets its first subscriber. + */ +struct LocalTrackSubscribedEvent { + /** Subscribed local track. */ + std::shared_ptr track; +}; + +/** + * Fired when a remote participant publishes a track. + */ +struct TrackPublishedEvent { + /** Remote track publication. */ + std::shared_ptr publication; + + /** Remote participant who owns this track (owned by Room). */ + RemoteParticipant *participant = nullptr; +}; + +/** + * Fired when a remote participant unpublishes a track. + */ +struct TrackUnpublishedEvent { + /** Remote track publication that was removed. */ + std::shared_ptr publication; + + /** Remote participant who owned this track (owned by Room). */ + RemoteParticipant *participant = nullptr; +}; + +/** + * Fired when a remote track is successfully subscribed. + */ +struct TrackSubscribedEvent { + /** Subscribed remote track. */ + std::shared_ptr track; + + /** Publication associated with the track. */ + std::shared_ptr publication; + + /** Remote participant who owns the track (owned by Room). */ + RemoteParticipant *participant = nullptr; +}; + +/** + * Fired when a remote track is unsubscribed. + */ +struct TrackUnsubscribedEvent { + /** Track that was unsubscribed. */ + std::shared_ptr track; + + /** Publication associated with the track. */ + std::shared_ptr publication; + + /** Remote participant who owns the track (owned by Room). */ + RemoteParticipant *participant = nullptr; +}; + +/** + * Fired when subscribing to a remote track fails. + */ +struct TrackSubscriptionFailedEvent { + /** Remote participant for which the subscription failed (owned by Room). */ + RemoteParticipant *participant = nullptr; + + /** SID of the track that failed to subscribe. */ + std::string track_sid; + + /** Error message describing the failure. */ + std::string error; +}; + +/** + * Fired when a track is muted. + */ +struct TrackMutedEvent { + /** Local or remote participant who owns the track (owned by Room). */ + Participant *participant = nullptr; + + /** Publication that was muted. */ + std::shared_ptr publication; +}; + +/** + * Fired when a track is unmuted. + */ +struct TrackUnmutedEvent { + /** Local or remote participant who owns the track (owned by Room). */ + Participant *participant = nullptr; + + /** Publication that was unmuted. */ + std::shared_ptr publication; +}; + +/** + * Fired when the list of active speakers changes. + */ +struct ActiveSpeakersChangedEvent { + /** Participants currently considered active speakers (owned by Room). */ + std::vector speakers; +}; + +/** + * Fired when room metadata is updated. + */ +struct RoomMetadataChangedEvent { + /** Previous metadata value. */ + std::string old_metadata; + + /** New metadata value. */ + std::string new_metadata; +}; + +/** + * Fired when the room SID changes (e.g., after migration). + */ +struct RoomSidChangedEvent { + /** New room SID. */ + std::string sid; +}; + +/** + * Fired when a participant's metadata is updated. + */ +struct ParticipantMetadataChangedEvent { + /** Participant whose metadata changed (owned by Room). */ + Participant *participant = nullptr; + + /** Old metadata value. */ + std::string old_metadata; + + /** New metadata value. */ + std::string new_metadata; +}; + +/** + * Fired when a participant's name changes. + */ +struct ParticipantNameChangedEvent { + /** Participant whose name changed (owned by Room). */ + Participant *participant = nullptr; + + /** Previous name. */ + std::string old_name; + + /** New name. */ + std::string new_name; +}; + +/** + * Fired when a participant's attributes change. + */ +struct ParticipantAttributesChangedEvent { + /** Participant whose attributes changed (owned by Room). */ + Participant *participant = nullptr; + + /** Set of attributes that changed (key/value pairs). */ + std::vector changed_attributes; +}; + +/** + * Fired when a participant's encryption status changes. + */ +struct ParticipantEncryptionStatusChangedEvent { + /** Participant whose encryption status changed (owned by Room). */ + Participant *participant = nullptr; + + /** True if the participant is now fully encrypted. */ + bool is_encrypted = false; +}; + +/** + * Fired when a participant's connection quality estimate changes. + */ +struct ConnectionQualityChangedEvent { + /** Participant whose connection quality changed (owned by Room). */ + Participant *participant = nullptr; + + /** New connection quality. */ + ConnectionQuality quality = ConnectionQuality::Good; +}; + +/** + * Fired when a user data packet (non-SIP) is received. + */ +struct UserDataPacketEvent { + /** Payload data. */ + std::vector data; + + /** Delivery kind (reliable or lossy). */ + DataPacketKind kind = DataPacketKind::Reliable; + + /** Remote participant that sent this packet, or nullptr if server (owned by + * Room). */ + RemoteParticipant *participant = nullptr; + + /** Optional topic associated with this data (may be empty). */ + std::string topic; +}; + +/** + * Fired when a SIP DTMF packet is received. + */ +struct SipDtmfReceivedEvent { + /** DTMF code. */ + int code = 0; + + /** Human-readable DTMF digit. */ + std::string digit; + + /** Remote participant that sent the DTMF (owned by Room). */ + RemoteParticipant *participant = nullptr; +}; + +/** + * One transcription unit with optional participant/track linkage. + */ +struct Transcription { + /** Optional identity of the participant who spoke. */ + std::optional participant_identity; + + /** Optional SID of the track associated with this transcription. */ + std::optional track_sid; + + /** Ordered segments that make up the transcription. */ + std::vector segments; +}; + +/** + * Fired when a transcription result is received. + */ +struct TranscriptionReceivedEvent { + /** Transcription segments for this update. */ + std::vector segments; + + /** Local or remote participant associated with these segments (owned by + * Room). */ + Participant *participant = nullptr; + + /** Publication of the track used for transcription, if available. */ + std::shared_ptr publication; +}; + +/** + * Fired when the room's connection state changes. + */ +struct ConnectionStateChangedEvent { + /** New connection state. */ + ConnectionState state = ConnectionState::Disconnected; +}; + +/** + * Fired when the room is disconnected. + */ +struct DisconnectedEvent { + /** Reason for disconnect, if known. */ + DisconnectReason reason = DisconnectReason::Unknown; +}; + +/** + * Fired just before attempting to reconnect. + */ +struct ReconnectingEvent {}; + +/** + * Fired after successfully reconnecting. + */ +struct ReconnectedEvent {}; + +/** + * Fired when the room has reached end-of-stream (no more events). + */ +struct RoomEosEvent {}; + +/** + * Fired when a data stream header is received. + */ +struct DataStreamHeaderReceivedEvent { + /** Identity of the participant that sent the stream. */ + std::string participant_identity; + + /** Parsed header data. */ + DataStreamHeaderData header; +}; + +/** + * Fired when a data stream chunk is received. + */ +struct DataStreamChunkReceivedEvent { + /** Identity of the participant that sent the stream. */ + std::string participant_identity; + + /** Chunk payload and metadata. */ + DataStreamChunkData chunk; +}; + +/** + * Fired when a data stream trailer is received. + */ +struct DataStreamTrailerReceivedEvent { + /** Identity of the participant that sent the stream. */ + std::string participant_identity; + + /** Trailer metadata describing the stream termination. */ + DataStreamTrailerData trailer; +}; + +/** + * Fired when a data channel's buffered amount falls below its low threshold. + */ +struct DataChannelBufferedAmountLowThresholdChangedEvent { + /** Data channel kind (reliable or lossy). */ + DataPacketKind kind = DataPacketKind::Reliable; + + /** New threshold value in bytes. */ + std::uint64_t threshold = 0; +}; + +/** + * Fired when a high-level byte stream reader is opened. + */ +struct ByteStreamOpenedEvent { + /** Handle to the underlying byte stream reader. */ + std::uint64_t reader_handle = 0; + + /** Identity of the participant that opened the stream. */ + std::string participant_identity; +}; + +/** + * Fired when a high-level text stream reader is opened. + */ +struct TextStreamOpenedEvent { + /** Handle to the underlying text stream reader. */ + std::uint64_t reader_handle = 0; + + /** Identity of the participant that opened the stream. */ + std::string participant_identity; +}; + +/** + * Fired when the room's info is updated. + */ +struct RoomUpdatedEvent { + /** New room info snapshot. */ + RoomInfoData info; +}; + +/** + * Fired when the participant has been moved to another room. + */ +struct RoomMovedEvent { + /** Info about the new room. */ + RoomInfoData info; +}; + +/** + * Fired when a batch of participants has been updated. + */ +struct ParticipantsUpdatedEvent { + /** Participants updated in this event (owned by Room). */ + std::vector participants; +}; + +/** + * Fired when a participant's E2EE state changes. + */ +struct E2eeStateChangedEvent { + /** Local or remote participant whose state changed (owned by Room). */ + Participant *participant = nullptr; + + /** New encryption state. */ + EncryptionState state = EncryptionState::New; +}; + +/** + * Fired when a chat message is received. + */ +struct ChatMessageReceivedEvent { + /** Chat message payload. */ + ChatMessageData message; + + /** Identity of the participant who sent the message. */ + std::string participant_identity; +}; + +} // namespace livekit diff --git a/src/remote_participant.cpp b/src/remote_participant.cpp index 753f28f..a784887 100644 --- a/src/remote_participant.cpp +++ b/src/remote_participant.cpp @@ -20,6 +20,8 @@ #include #include +#include "livekit/remote_track_publication.h" + namespace livekit { RemoteParticipant::RemoteParticipant( diff --git a/src/room.cpp b/src/room.cpp index 4f29583..0298eb5 100644 --- a/src/room.cpp +++ b/src/room.cpp @@ -25,6 +25,7 @@ #include "livekit/remote_track_publication.h" #include "livekit/remote_video_track.h" #include "livekit/room_delegate.h" +#include "livekit/room_event_types.h" #include "livekit/video_stream.h" #include "ffi.pb.h" @@ -204,10 +205,6 @@ void Room::OnEvent(const FfiEvent &event) { switch (event.message_case()) { case FfiEvent::kRoomEvent: { const proto::RoomEvent &re = event.room_event(); - - // Optional generic hook - delegate_snapshot->onRoomEvent(*this); - switch (re.message_case()) { case proto::RoomEvent::kParticipantConnected: { std::shared_ptr new_participant; diff --git a/src/room_proto_converter.h b/src/room_proto_converter.h index 14b58f8..8b3e4f0 100644 --- a/src/room_proto_converter.h +++ b/src/room_proto_converter.h @@ -16,7 +16,7 @@ #pragma once -#include "livekit/room_delegate.h" +#include "livekit/room_event_types.h" #include "room.pb.h" #include From ed4c40ef89006b5cb20fc6893c4ae53d55b138c7 Mon Sep 17 00:00:00 2001 From: shijing xian Date: Thu, 11 Dec 2025 13:21:56 -0800 Subject: [PATCH 12/12] removed the deprecated events --- include/livekit/room_delegate.h | 12 -------- include/livekit/room_event_types.h | 49 ------------------------------ src/room.cpp | 37 ++-------------------- src/room_proto_converter.cpp | 39 ------------------------ src/room_proto_converter.h | 6 ---- 5 files changed, 2 insertions(+), 141 deletions(-) diff --git a/include/livekit/room_delegate.h b/include/livekit/room_delegate.h index 51b6c40..04474a9 100644 --- a/include/livekit/room_delegate.h +++ b/include/livekit/room_delegate.h @@ -244,18 +244,6 @@ class RoomDelegate { */ virtual void onSipDtmfReceived(Room &, const SipDtmfReceivedEvent &) {} - /** - * Called when a transcription result is received. - */ - virtual void onTranscriptionReceived(Room &, - const TranscriptionReceivedEvent &) {} - - /** - * Called when a chat message is received. - */ - virtual void onChatMessageReceived(Room &, const ChatMessageReceivedEvent &) { - } - // ------------------------------------------------------------------ // Data streams // ------------------------------------------------------------------ diff --git a/include/livekit/room_event_types.h b/include/livekit/room_event_types.h index e448245..d70c060 100644 --- a/include/livekit/room_event_types.h +++ b/include/livekit/room_event_types.h @@ -105,29 +105,6 @@ enum class DisconnectReason { MediaFailure }; -/** - * A chat message associated with the room. - */ -struct ChatMessageData { - /** Unique ID of the message. */ - std::string id; - - /** Timestamp (ms since Unix epoch). */ - std::int64_t timestamp = 0; - - /** Message body. */ - std::string message; - - /** Optional timestamp when the message was edited (ms since Unix epoch). */ - std::optional edit_timestamp; - - /** True if the message has been deleted. */ - bool deleted = false; - - /** True if the message was generated (e.g. by an AI or system). */ - bool generated = false; -}; - /** * Application-level user data carried in a data packet. */ @@ -647,21 +624,6 @@ struct Transcription { std::vector segments; }; -/** - * Fired when a transcription result is received. - */ -struct TranscriptionReceivedEvent { - /** Transcription segments for this update. */ - std::vector segments; - - /** Local or remote participant associated with these segments (owned by - * Room). */ - Participant *participant = nullptr; - - /** Publication of the track used for transcription, if available. */ - std::shared_ptr publication; -}; - /** * Fired when the room's connection state changes. */ @@ -794,15 +756,4 @@ struct E2eeStateChangedEvent { EncryptionState state = EncryptionState::New; }; -/** - * Fired when a chat message is received. - */ -struct ChatMessageReceivedEvent { - /** Chat message payload. */ - ChatMessageData message; - - /** Identity of the participant who sent the message. */ - std::string participant_identity; -}; - } // namespace livekit diff --git a/src/room.cpp b/src/room.cpp index 0298eb5..eaf82f0 100644 --- a/src/room.cpp +++ b/src/room.cpp @@ -773,39 +773,7 @@ void Room::OnEvent(const FfiEvent &event) { // ------------------------------------------------------------------------ case proto::RoomEvent::kTranscriptionReceived: { - TranscriptionReceivedEvent ev; - { - std::lock_guard guard(lock_); - const auto &tr = re.transcription_received(); - for (const auto &s : tr.segments()) { - TranscriptionSegment seg; - seg.id = s.id(); - seg.text = s.text(); - seg.final = s.final(); - seg.start_time = s.start_time(); - seg.end_time = s.end_time(); - seg.language = s.language(); - ev.segments.push_back(std::move(seg)); - } - - Participant *participant = nullptr; - if (!tr.participant_identity().empty()) { - const std::string &identity = tr.participant_identity(); - if (local_participant_ && - local_participant_->identity() == identity) { - participant = local_participant_.get(); - } else { - auto it = remote_participants_.find(identity); - if (it != remote_participants_.end()) { - participant = it->second.get(); - } - } - } - ev.participant = participant; - ev.publication = participant->findTrackPublication(tr.track_sid()); - } - - delegate_snapshot->onTranscriptionReceived(*this, ev); + // Deprecated event, do nothing. break; } @@ -901,8 +869,7 @@ void Room::OnEvent(const FfiEvent &event) { break; } case proto::RoomEvent::kChatMessage: { - auto ev = fromProto(re.chat_message()); - delegate_snapshot->onChatMessageReceived(*this, ev); + // Deprecated event, do nothing. break; } case proto::RoomEvent::kStreamHeaderReceived: { diff --git a/src/room_proto_converter.cpp b/src/room_proto_converter.cpp index 9173de6..9da3d24 100644 --- a/src/room_proto_converter.cpp +++ b/src/room_proto_converter.cpp @@ -69,23 +69,6 @@ DisconnectReason toDisconnectReason(proto::DisconnectReason /*in*/) { // --------- basic helper conversions --------- -ChatMessageData fromProto(const proto::ChatMessage &in) { - ChatMessageData out; - out.id = in.id(); - out.timestamp = in.timestamp(); - out.message = in.message(); - if (in.has_edit_timestamp()) { - out.edit_timestamp = in.edit_timestamp(); - } - if (in.has_deleted()) { - out.deleted = in.deleted(); - } - if (in.has_generated()) { - out.generated = in.generated(); - } - return out; -} - UserPacketData fromProto(const proto::UserPacket &in) { UserPacketData out; // TODO, double check following code is safe @@ -299,13 +282,6 @@ RoomMovedEvent roomMovedFromProto(const proto::RoomInfo &in) { return ev; } -ChatMessageReceivedEvent fromProto(const proto::ChatMessageReceived &in) { - ChatMessageReceivedEvent ev; - ev.message = fromProto(in.message()); - ev.participant_identity = in.participant_identity(); - return ev; -} - // ---------------- Room Options ---------------- proto::AudioEncoding toProto(const AudioEncodingOptions &in) { @@ -420,21 +396,6 @@ TranscriptionSegment fromProto(const proto::TranscriptionSegment &in) { return out; } -Transcription fromProto(const proto::TranscriptionReceived &in) { - Transcription out; - if (in.has_participant_identity()) { - out.participant_identity = in.participant_identity(); - } - if (in.has_track_sid()) { - out.track_sid = in.track_sid(); - } - out.segments.reserve(in.segments_size()); - for (const auto &pseg : in.segments()) { - out.segments.push_back(fromProto(pseg)); - } - return out; -} - UserDataPacketEvent userDataPacketFromProto(const proto::DataPacketReceived &in, RemoteParticipant *participant) { UserDataPacketEvent ev; diff --git a/src/room_proto_converter.h b/src/room_proto_converter.h index 8b3e4f0..0538207 100644 --- a/src/room_proto_converter.h +++ b/src/room_proto_converter.h @@ -33,7 +33,6 @@ ConnectionState toConnectionState(proto::ConnectionState in); DataPacketKind toDataPacketKind(proto::DataPacketKind in); DisconnectReason toDisconnectReason(proto::DisconnectReason in); -ChatMessageData fromProto(const proto::ChatMessage &in); UserPacketData fromProto(const proto::UserPacket &in); SipDtmfData fromProto(const proto::SipDTMF &in); RoomInfoData fromProto(const proto::RoomInfo &in); @@ -69,8 +68,6 @@ RoomUpdatedEvent roomUpdatedFromProto(const proto::RoomInfo &in); // room_updated RoomMovedEvent roomMovedFromProto(const proto::RoomInfo &in); // moved -ChatMessageReceivedEvent fromProto(const proto::ChatMessageReceived &in); - // --------- room options conversions --------- proto::AudioEncoding toProto(const AudioEncodingOptions &in); @@ -87,9 +84,6 @@ TrackPublishOptions fromProto(const proto::TrackPublishOptions &in); proto::TranscriptionSegment toProto(const TranscriptionSegment &in); TranscriptionSegment fromProto(const proto::TranscriptionSegment &in); -proto::TranscriptionReceived toProto(const TranscriptionReceivedEvent &in); -Transcription fromProto(const proto::TranscriptionReceived &in); - // --------- room Data Packet conversions --------- UserDataPacketEvent userDataPacketFromProto(const proto::DataPacketReceived &in,