diff --git a/.github/workflows/builds.yml b/.github/workflows/builds.yml new file mode 100644 index 0000000..86aaa73 --- /dev/null +++ b/.github/workflows/builds.yml @@ -0,0 +1,125 @@ +name: Builds + +on: + push: + branches: ["main"] + paths: + - src/** + - include/** + - examples/** + - client-sdk-rust/** + - CMakeLists.txt + - build.sh + - .github/workflows/** + pull_request: + branches: ["main"] + paths: + - src/** + - include/** + - examples/** + - client-sdk-rust/** + - CMakeLists.txt + - build.sh + - .github/workflows/** + workflow_dispatch: + +env: + CARGO_TERM_COLOR: always + +jobs: + build: + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-latest + - os: macos-latest + # Windows needs protobuf *libraries* too (not just protoc). + # Enable after wiring vcpkg (see notes below). + # - os: windows-latest + name: ${{ matrix.os }} + runs-on: ${{ matrix.os }} + + steps: + - name: Checkout (with submodules) + uses: actions/checkout@v4 + with: + submodules: recursive + fetch-depth: 0 + + # ---------- OS-specific deps: install protoc + libprotobuf that MATCH ---------- + - name: Install deps (Ubuntu) + if: runner.os == 'Linux' + run: | + set -eux + sudo apt-get update + sudo apt-get install -y cmake ninja-build build-essential \ + protobuf-compiler libprotobuf-dev libabsl-dev \ + libx11-dev libxext-dev libgl1-mesa-dev libssl-dev + protoc --version + pkg-config --modversion protobuf + # Fail if versions don't match (best-effort) + test "$(protoc --version | awk '{print $2}')" = "$(pkg-config --modversion protobuf)" || { + echo "protoc and libprotobuf versions differ"; exit 1; } + + - name: Install deps (macOS) + if: runner.os == 'macOS' + run: | + set -eux + brew update + # EITHER: latest protobuf + brew install cmake protobuf ninja + # OR pin a specific version (e.g., protobuf@6) if you need it: + # brew install protobuf@6 && brew link --overwrite --force protobuf@6 + protoc --version + pkg-config --modversion protobuf + + # ---------- Rust toolchain ---------- + - name: Install Rust (stable) + uses: dtolnay/rust-toolchain@stable + + # ---------- Cache Cargo ---------- + - name: Cache Cargo registry + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + key: ${{ runner.os }}-cargo-reg-${{ hashFiles('**/Cargo.lock') }} + restore-keys: ${{ runner.os }}-cargo-reg- + + - name: Cache Cargo target + uses: actions/cache@v4 + with: + path: client-sdk-rust/target + key: ${{ runner.os }}-cargo-target-${{ hashFiles('**/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo-target- + + # ---------- Build (Debug / Release) ---------- + - name: Build Debug + shell: bash + run: | + chmod +x build.sh + ./build.sh debug + + - name: Build Release + shell: bash + run: | + ./build.sh release + + # ---------- Smoke test example (no server needed) ---------- + - name: Smoke test example (Debug) + if: runner.os != 'Windows' + shell: bash + run: | + if [[ -x build/examples/SimpleRoom ]]; then + build/examples/SimpleRoom --help || true + fi + + # ---------- Cleanup ---------- + - name: Clean after build (best-effort) + if: always() + shell: bash + run: | + [[ -x build.sh ]] && ./build.sh clean-all || true diff --git a/.gitignore b/.gitignore index cdc99b9..3bc6e49 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ CMakeCache.txt Makefile cmake_install.cmake out +build/ diff --git a/CMakeLists.txt b/CMakeLists.txt index 24ef94b..dd989e7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,56 +1,260 @@ -cmake_minimum_required(VERSION 3.0) -project(livekit) +cmake_minimum_required(VERSION 3.31.0) +project(livekit LANGUAGES C CXX) +# ---- C++ standard ---- set(CMAKE_CXX_STANDARD 17) -set(FFI_PROTO_PATH client-sdk-rust/livekit-ffi/protocol) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_POSITION_INDEPENDENT_CODE ON) + +# ---- Protobuf (FFI protos) ---- +set(FFI_PROTO_DIR ${CMAKE_SOURCE_DIR}/client-sdk-rust/livekit-ffi/protocol) set(FFI_PROTO_FILES - ${FFI_PROTO_PATH}/handle.proto - ${FFI_PROTO_PATH}/ffi.proto - ${FFI_PROTO_PATH}/participant.proto - ${FFI_PROTO_PATH}/room.proto - ${FFI_PROTO_PATH}/track.proto - ${FFI_PROTO_PATH}/video_frame.proto - ${FFI_PROTO_PATH}/audio_frame.proto -) -set(PROTO_BINARY_DIR ${CMAKE_CURRENT_BINARY_DIR}/generated) + ${FFI_PROTO_DIR}/handle.proto + ${FFI_PROTO_DIR}/ffi.proto + ${FFI_PROTO_DIR}/participant.proto + ${FFI_PROTO_DIR}/room.proto + ${FFI_PROTO_DIR}/track.proto + ${FFI_PROTO_DIR}/video_frame.proto + ${FFI_PROTO_DIR}/audio_frame.proto +) +set(PROTO_BINARY_DIR ${CMAKE_BINARY_DIR}/generated) file(MAKE_DIRECTORY ${PROTO_BINARY_DIR}) -find_package(Protobuf REQUIRED) +find_package(Protobuf REQUIRED) # protobuf::libprotobuf, protoc +find_package(absl CONFIG REQUIRED) -# livekit-proto +# Object library that owns generated .pb.cc/.pb.h add_library(livekit_proto OBJECT ${FFI_PROTO_FILES}) +target_include_directories(livekit_proto PUBLIC + "$" + ${Protobuf_INCLUDE_DIRS} +) target_link_libraries(livekit_proto PUBLIC protobuf::libprotobuf) -target_include_directories(livekit_proto PUBLIC "$") -target_include_directories(livekit_proto PUBLIC ${Protobuf_INCLUDE_DIRS}) +# Generate .pb sources into ${PROTO_BINARY_DIR} and attach to livekit_proto protobuf_generate( - TARGET livekit_proto - PROTOS ${FFI_PROTO_FILES} - APPEND_PATH ${FFI_PROTO_PATH} - IMPORT_DIRS ${FFI_PROTO_PATH} - PROTOC_OUT_DIR ${PROTO_BINARY_DIR} + LANGUAGE cpp + TARGET livekit_proto + PROTOC_OUT_DIR ${PROTO_BINARY_DIR} + IMPORT_DIRS ${FFI_PROTO_DIR} +) + +# Find cargo +find_program(CARGO_EXECUTABLE NAMES cargo REQUIRED) + +set(RUST_ROOT ${CMAKE_SOURCE_DIR}/client-sdk-rust) + +# Write a helper script that never passes empty args +set(RUN_CARGO_SCRIPT ${CMAKE_BINARY_DIR}/run_cargo.cmake) +file(WRITE ${RUN_CARGO_SCRIPT} +"if(NOT DEFINED CFG) + set(CFG Debug) +endif() +if(NOT DEFINED RUST_ROOT) + message(FATAL_ERROR \"RUST_ROOT not set\") +endif() +if(NOT DEFINED CARGO) + message(FATAL_ERROR \"CARGO not set\") +endif() + +# Build arg list +set(ARGS build) +if(NOT CFG STREQUAL \"Debug\") + list(APPEND ARGS --release) +endif() + +message(STATUS \"[run_cargo.cmake] CFG=\${CFG} CARGO=\${CARGO}\") +execute_process( + COMMAND \"\${CARGO}\" \${ARGS} + WORKING_DIRECTORY \"\${RUST_ROOT}\" + RESULT_VARIABLE rv +) +if(rv) + message(FATAL_ERROR \"cargo build failed with code: \${rv}\") +endif() +") + +# Imported Rust lib with per-config locations +add_library(livekit_ffi STATIC IMPORTED GLOBAL) +set_target_properties(livekit_ffi PROPERTIES + IMPORTED_LOCATION_DEBUG "${RUST_ROOT}/target/debug/liblivekit_ffi.a" + IMPORTED_LOCATION_RELWITHDEBINFO "${RUST_ROOT}/target/release/liblivekit_ffi.a" + IMPORTED_LOCATION_MINSIZEREL "${RUST_ROOT}/target/release/liblivekit_ffi.a" + IMPORTED_LOCATION_RELEASE "${RUST_ROOT}/target/release/liblivekit_ffi.a" + INTERFACE_INCLUDE_DIRECTORIES "${RUST_ROOT}/livekit-ffi/include" +) + +# Custom target that runs the script; no empty args get passed to cargo +add_custom_target(build_rust_ffi ALL + COMMAND "${CMAKE_COMMAND}" + -DCFG=$ + -DRUST_ROOT=${RUST_ROOT} + -DCARGO=${CARGO_EXECUTABLE} + -P "${RUN_CARGO_SCRIPT}" + USES_TERMINAL + COMMENT "Invoking cargo for Rust FFI ($)" + VERBATIM +) + +# ---- C++ wrapper library ---- +add_library(livekit + include/livekit/room.h + include/livekit/ffi_client.h + include/livekit/livekit.h + src/ffi_client.cpp + src/room.cpp +) + +# Add generated proto objects to the wrapper +target_sources(livekit PRIVATE $) + +target_include_directories(livekit PUBLIC + ${CMAKE_SOURCE_DIR}/include + ${RUST_ROOT}/livekit-ffi/include + ${PROTO_BINARY_DIR} ) -# livekit -add_library(livekit - include/livekit/room.h - include/livekit/ffi_client.h - include/livekit/livekit.h - src/ffi_client.cpp - src/room.cpp - ${PROTO_SRCS} - ${PROTO_HEADERS} - ${PROTO_FILES} +target_link_libraries(livekit + PUBLIC + livekit_ffi + protobuf::libprotobuf ) +# Ensure protoc matches headers/libs +message(STATUS "Protobuf: version=${Protobuf_VERSION}; protoc=${Protobuf_PROTOC_EXECUTABLE}") + +# Ensure cargo runs before we try to link livekit +add_dependencies(livekit build_rust_ffi) + + +########### Platform specific settings ########### + +if(APPLE) + find_library(FW_COREAUDIO CoreAudio REQUIRED) + find_library(FW_AUDIOTOOLBOX AudioToolbox REQUIRED) + find_library(FW_COREFOUNDATION CoreFoundation REQUIRED) + find_library(FW_SECURITY Security REQUIRED) + find_library(FW_COREGRAPHICS CoreGraphics REQUIRED) + find_library(FW_COREMEDIA CoreMedia REQUIRED) + find_library(FW_VIDEOTOOLBOX VideoToolbox REQUIRED) + find_library(FW_AVFOUNDATION AVFoundation REQUIRED) + find_library(FW_COREVIDEO CoreVideo REQUIRED) + find_library(FW_FOUNDATION Foundation REQUIRED) + find_library(FW_APPKIT AppKit REQUIRED) + find_library(FW_QUARTZCORE QuartzCore REQUIRED) + find_library(FW_OPENGL OpenGL REQUIRED) + find_library(FW_IOSURFACE IOSurface REQUIRED) + find_library(FW_METAL Metal REQUIRED) + find_library(FW_METALKIT MetalKit REQUIRED) + + target_link_libraries(livekit PUBLIC + ${FW_COREAUDIO} + ${FW_AUDIOTOOLBOX} + ${FW_COREFOUNDATION} + ${FW_SECURITY} + ${FW_COREGRAPHICS} + ${FW_COREMEDIA} + ${FW_VIDEOTOOLBOX} + ${FW_AVFOUNDATION} + ${FW_COREVIDEO} + ${FW_FOUNDATION} + ${FW_APPKIT} + ${FW_QUARTZCORE} + ${FW_OPENGL} + ${FW_IOSURFACE} + ${FW_METAL} + ${FW_METALKIT} + ) + + # Ensure Objective-C categories/classes in static archives are loaded (WebRTC needs this) + # Use LINKER: to guarantee it is passed to the linker (CMake >= 3.13) + target_link_options(livekit INTERFACE "LINKER:-ObjC") +endif() +# Only Protobuf >= 6 needs Abseil +if (Protobuf_VERSION VERSION_GREATER_EQUAL 6.0) + # Try modern package name/namespace + find_package(absl CONFIG QUIET) + # Some distros export as "Abseil::" + if (NOT absl_FOUND) + find_package(Abseil QUIET) + endif() -# Include the auto-generated files from livekit-ffi (C headers) -target_include_directories(livekit PUBLIC client-sdk-rust/livekit-ffi/include/) -target_include_directories(livekit PUBLIC include/) + if (absl_FOUND) + target_link_libraries(livekit PUBLIC + absl::log + absl::check + absl::strings + absl::base + ) + elseif (Abseil_FOUND) + target_link_libraries(livekit PUBLIC + Abseil::log + Abseil::check + Abseil::strings + Abseil::base + ) + else() + message(FATAL_ERROR + "Protobuf ${Protobuf_VERSION} requires Abseil but no CMake package was found.\n" + "Install Abseil (macOS: 'brew install abseil', Ubuntu: 'sudo apt-get install libabsl-dev'), " + "or use Protobuf < 6.") + endif() +else() + message(STATUS "Protobuf < 6 detected; skipping Abseil linking.") +endif() -# Link against livekit-ffi -link_directories(${CMAKE_CURRENT_BINARY_DIR}) -target_link_libraries(livekit PUBLIC livekit_ffi livekit_proto) +# On Linux, it needs to link OpenSSL +if(UNIX AND NOT APPLE) + find_package(OpenSSL REQUIRED) + target_link_libraries(livekit PUBLIC OpenSSL::SSL OpenSSL::Crypto) +endif() -# Examples + +# Warnings +if (MSVC) + target_compile_options(livekit PRIVATE /permissive- /Zc:__cplusplus /W4) +else() + target_compile_options(livekit PRIVATE -Wall -Wextra -Wpedantic) +endif() + +# ---- Examples ---- add_subdirectory(examples) + + +# ---------- Clean helpers ---------- +# Removes generated protobuf sources +add_custom_target(clean_generated + COMMAND ${CMAKE_COMMAND} -E echo "Removing generated protobufs: ${PROTO_BINARY_DIR}" + COMMAND ${CMAKE_COMMAND} -E rm -rf "${PROTO_BINARY_DIR}" + COMMENT "Clean generated protobuf files" + VERBATIM +) + +# Cargo clean (safer, lets Cargo decide what to delete) +add_custom_target(cargo_clean + COMMAND ${CMAKE_COMMAND} -E echo "Running 'cargo clean' in: ${RUST_ROOT}" + COMMAND "${CARGO_EXECUTABLE}" clean + WORKING_DIRECTORY "${RUST_ROOT}" + COMMENT "Clean Rust target directory via cargo" + VERBATIM +) + +# Combined "clean-all" (C++ clean + generated + cargo) +# Note: 'clean' is CMake's built-in target that removes CMake-built artifacts. +add_custom_target(clean_all + # 1) CMake clean (object files, libs, exes) + COMMAND ${CMAKE_COMMAND} -E echo "==> CMake clean in: ${CMAKE_BINARY_DIR}" + COMMAND ${CMAKE_COMMAND} --build "${CMAKE_BINARY_DIR}" --target clean || true + # 2) Cargo clean (Rust target/) + COMMAND ${CMAKE_COMMAND} -E echo "==> cargo clean in: ${RUST_ROOT}" + COMMAND ${CMAKE_COMMAND} -E chdir "${RUST_ROOT}" "${CARGO_EXECUTABLE}" clean || true + # 3) Remove generated protobufs (lives under build/) + COMMAND ${CMAKE_COMMAND} -E echo "==> removing generated protobufs: ${PROTO_BINARY_DIR}" + COMMAND ${CMAKE_COMMAND} -E rm -rf "${PROTO_BINARY_DIR}" || true + # 4) Remove the entire build directory (like `rm -rf build`) + # Switch to SOURCE dir first so removing BINARY dir is always safe. + COMMAND ${CMAKE_COMMAND} -E echo "==> removing build directory: ${CMAKE_BINARY_DIR}" + COMMAND ${CMAKE_COMMAND} -E chdir "${CMAKE_SOURCE_DIR}" ${CMAKE_COMMAND} -E rm -rf "${CMAKE_BINARY_DIR}" || true + COMMENT "Full clean: CMake outputs + Rust target + generated protos + delete build/" + VERBATIM +) \ No newline at end of file diff --git a/README.md b/README.md index 3d2569f..3fd4ff6 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,66 @@ # LiveKit C++ Client SDK -This repo is a work-in-progress. It's not ready to be used yet. We are building this one in public in collaboration with the community. +This SDK enables native C++ applications to connect to LiveKit servers for real-time audio/video communication. -Interested in contributing? Stop by our [Community Slack](https://livekit.io/join-slack) and join the #dev channel +--- + +## 📦 Requirements +- **CMake** ≥ 4.0 +- **Rust / Cargo** (latest stable toolchain) +- **Protobuf** compiler (`protoc`) +- **macOS** users: System frameworks (CoreAudio, AudioToolbox, etc.) are automatically linked via CMake. + + +## 🧩 Clone the Repository + +Make sure to initialize the Rust submodule (`client-sdk-rust`): + +```bash +# Option 1: Clone with submodules in one step +git clone --recurse-submodules https://github.com/livekit/client-sdk-cpp.git + +# Option 2: Clone first, then initialize submodules +git clone https://github.com/livekit/client-sdk-cpp.git +cd client-sdk-cpp +git submodule update --init --recursive +``` + +## ⚙️ BUILD + +All build actions are managed by the provided build.sh script. +```bash +./build.sh clean # Clean CMake build artifacts +./build.sh clean-all # Deep clean (C++ + Rust + generated files) +./build.sh debug # Build Debug version +./build.sh release # Build Release version +./build.sh verbose # Verbose build output +``` + +## 🧪 Run Example + +```bash +./build/examples/SimpleRoom --url ws://localhost:7880 --token +``` + +You can also provide the URL and token via environment variables: +```bash +export LIVEKIT_URL=ws://localhost:7880 +export LIVEKIT_TOKEN= +./build/examples/SimpleRoom +``` + +Press Ctrl-C to exit the example. + + +## 🧰 Recommended Setup +### macOS +```bash +brew install cmake protobuf rust +``` + +### Ubuntu / Debian +```bash +sudo apt update +sudo apt install -y cmake protobuf-compiler build-essential +curl https://sh.rustup.rs -sSf | sh +``` \ No newline at end of file diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..0a41604 --- /dev/null +++ b/build.sh @@ -0,0 +1,104 @@ +#!/usr/bin/env bash +set -euo pipefail + +PROJECT_ROOT="$(cd "$(dirname "$0")" && pwd)" +BUILD_DIR="${PROJECT_ROOT}/build" +BUILD_TYPE="Release" +VERBOSE="" +TARGET="" + + +usage() { + cat < Configuring CMake (${BUILD_TYPE})..." + cmake -S . -B "${BUILD_DIR}" -DCMAKE_BUILD_TYPE="${BUILD_TYPE}" +} + +build() { + echo "==> Building (${BUILD_TYPE})..." + cmake --build "${BUILD_DIR}" -j ${VERBOSE:+--verbose} +} + +clean() { + echo "==> Cleaning CMake targets..." + if [[ -d "${BUILD_DIR}" ]]; then + cmake --build "${BUILD_DIR}" --target clean || true + else + echo " (skipping) ${BUILD_DIR} does not exist." + fi +} + +clean_all() { + echo "==> Running full clean-all (C++ + Rust)..." + if [[ -d "${BUILD_DIR}" ]]; then + cmake --build "${BUILD_DIR}" --target clean_all || true + else + echo " (info) ${BUILD_DIR} does not exist; doing manual deep clean..." + fi + + rm -rf "${PROJECT_ROOT}/client-sdk-rust/target/debug" || true + rm -rf "${PROJECT_ROOT}/client-sdk-rust/target/release" || true + rm -rf "${BUILD_DIR}" || true + echo "==> Clean-all complete." +} + + +if [[ $# -eq 0 ]]; then + usage + exit 0 +fi + +case "$1" in + debug) + BUILD_TYPE="Debug" + configure + build + ;; + release) + BUILD_TYPE="Release" + configure + build + ;; + verbose) + VERBOSE="1" + build + ;; + clean) + clean + ;; + clean-all) + clean_all + ;; + distclean) + distclean + ;; + help|-h|--help) + usage + ;; + *) + echo "Unknown command: $1" + usage + exit 1 + ;; +esac + diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index cc9a8a4..92e35c7 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required (VERSION 3.0) +cmake_minimum_required(VERSION 3.31.0) project (livekit-examples) add_executable(SimpleRoom simple_room/main.cpp) diff --git a/examples/simple_room/main.cpp b/examples/simple_room/main.cpp index e9e7796..045164a 100644 --- a/examples/simple_room/main.cpp +++ b/examples/simple_room/main.cpp @@ -1,20 +1,116 @@ +#include +#include +#include +#include +#include +#include +#include +#include + #include "livekit/livekit.h" using namespace livekit; -int main(int argc, char *argv[]) -{ - Room room{}; - room.Connect("ws://localhost:7880", "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjQ5MjM1OTQ4MjMsImlzcyI6IkFQSVRzRWZpZFpqclFvWSIsIm5hbWUiOiJ3ZWIiLCJuYmYiOjE2ODM1OTQ4MjMsInN1YiI6IndlYiIsInZpZGVvIjp7InJvb20iOiJsaXZla2l0LWZmaS10ZXN0Iiwicm9vbUpvaW4iOnRydWV9fQ.voLT9RK3wNYEGdWovPLv1BzyN1v5tpJ59e0DIqIVfiU"); +namespace { +std::atomic g_running{true}; + +void print_usage(const char* prog) { + std::cerr << "Usage:\n" + << " " << prog << " \n" + << "or:\n" + << " " << prog << " --url= --token=\n" + << " " << prog << " --url --token \n\n" + << "Env fallbacks:\n" + << " LIVEKIT_URL, LIVEKIT_TOKEN\n"; +} +void handle_sigint(int) { + g_running = false; +} - // Should we implement a mechanism to PollEvents/WaitEvents? Like SDL2/glfw - // - So we can remove the useless loop here - // Or is it better to use callback based events? +bool parse_args(int argc, char* argv[], std::string& url, std::string& token) { + // 1) --help + for (int i = 1; i < argc; ++i) { + std::string a = argv[i]; + if (a == "-h" || a == "--help") { + return false; + } + } + + // 2) flags: --url= / --token= or split form + 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 {}; + }; + + 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; + } + } + + // 3) positional if still empty + if (url.empty() || token.empty()) { + std::vector pos; + for (int i = 1; i < argc; ++i) { + std::string a = argv[i]; + if (a.rfind("--", 0) == 0) continue; // skip flags we already parsed + pos.push_back(std::move(a)); + } + if (pos.size() >= 2) { + if (url.empty()) url = pos[0]; + if (token.empty()) token = pos[1]; + } + } - while(true) { + // 4) env fallbacks + 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; + } + + return !(url.empty() || token.empty()); +} +} // namespace + +int main(int argc, char* argv[]) { + std::string url, token; + if (!parse_args(argc, argv, url, token)) { + print_usage(argv[0]); + return 1; + } + + std::cout << "Connecting to: " << url << std::endl; + + // Handle Ctrl-C to exit the idle loop + std::signal(SIGINT, handle_sigint); + + Room room{}; + room.Connect(url.c_str(), token.c_str()); + // TODO: replace with proper event loop / callbacks. + // For now, keep the app alive until Ctrl-C. + while (g_running.load()) { + std::this_thread::sleep_for(std::chrono::milliseconds(100)); } + std::cout << "Exiting.\n"; return 0; }