diff --git a/include/livekit/data_track_error.h b/include/livekit/data_track_error.h index 5ec8f97f..2982546f 100644 --- a/include/livekit/data_track_error.h +++ b/include/livekit/data_track_error.h @@ -14,8 +14,7 @@ * limitations under the License. */ -#ifndef LIVEKIT_DATA_TRACK_ERROR_H -#define LIVEKIT_DATA_TRACK_ERROR_H +#pragma once #include #include @@ -84,5 +83,3 @@ struct SubscribeDataTrackError { }; } // namespace livekit - -#endif // LIVEKIT_DATA_TRACK_ERROR_H diff --git a/include/livekit/local_audio_track.h b/include/livekit/local_audio_track.h index d8ad51e4..88147c00 100644 --- a/include/livekit/local_audio_track.h +++ b/include/livekit/local_audio_track.h @@ -87,7 +87,7 @@ class LocalAudioTrack : public Track { /// Sets the publication that owns this track. /// Note: std::move on a const& silently falls back to a copy, so we assign /// directly. Changing the virtual signature to take by value would enable - /// a true move but is an API-breaking change left for a future revision. + /// a true move but is a API-breaking change hence left for a future revision. void setPublication(const std::shared_ptr &publication) noexcept override { local_publication_ = publication; diff --git a/include/livekit/result.h b/include/livekit/result.h index dce0e874..809ce59f 100644 --- a/include/livekit/result.h +++ b/include/livekit/result.h @@ -14,8 +14,7 @@ * limitations under the License. */ -#ifndef LIVEKIT_RESULT_H -#define LIVEKIT_RESULT_H +#pragma once #include #include @@ -190,5 +189,3 @@ template class [[nodiscard]] Result { }; } // namespace livekit - -#endif // LIVEKIT_RESULT_H diff --git a/include/livekit/room.h b/include/livekit/room.h index e65d7d1f..07770f13 100644 --- a/include/livekit/room.h +++ b/include/livekit/room.h @@ -14,8 +14,7 @@ * limitations under the License. */ -#ifndef LIVEKIT_ROOM_H -#define LIVEKIT_ROOM_H +#pragma once #include "livekit/data_stream.h" #include "livekit/e2ee.h" @@ -173,6 +172,9 @@ class Room { /// Returns a snapshot of all current remote participants. std::vector> remoteParticipants() const; + /// Returns the current connection state of the room. + ConnectionState connectionState() const; + /* Register a handler for incoming text streams on a specific topic. * * When a remote participant opens a text stream with the given topic, @@ -343,5 +345,3 @@ class Room { void OnEvent(const proto::FfiEvent &event); }; } // namespace livekit - -#endif /* LIVEKIT_ROOM_H */ diff --git a/include/livekit/subscription_thread_dispatcher.h b/include/livekit/subscription_thread_dispatcher.h index 8e5fa65c..236975de 100644 --- a/include/livekit/subscription_thread_dispatcher.h +++ b/include/livekit/subscription_thread_dispatcher.h @@ -14,8 +14,7 @@ * limitations under the License. */ -#ifndef LIVEKIT_SUBSCRIPTION_THREAD_DISPATCHER_H -#define LIVEKIT_SUBSCRIPTION_THREAD_DISPATCHER_H +#pragma once #include "livekit/audio_stream.h" #include "livekit/video_stream.h" @@ -491,5 +490,3 @@ class SubscriptionThreadDispatcher { }; } // namespace livekit - -#endif /* LIVEKIT_SUBSCRIPTION_THREAD_DISPATCHER_H */ diff --git a/include/livekit/video_stream.h b/include/livekit/video_stream.h index 850b5038..50e2c17f 100644 --- a/include/livekit/video_stream.h +++ b/include/livekit/video_stream.h @@ -48,17 +48,17 @@ namespace proto { class FfiEvent; } -// Represents a pull-based stream of decoded PCM audio frames coming from -// a remote (or local) LiveKit track. Similar to VideoStream, but for audio. +// Represents a pull-based stream of decoded video frames coming from +// a remote (or local) LiveKit track. Similar to AudioStream, but for video. // // Typical usage: // // VideoStream::Options opts; // auto stream = VideoStream::fromTrack(remoteVideoTrack, opts); // -// AudioFrameEvent ev; +// VideoFrameEvent ev; // while (stream->read(ev)) { -// // ev.frame contains interleaved int16 PCM samples +// // ev.frame contains the decoded video buffer // } // // stream->close(); // optional, called automatically in destructor diff --git a/src/room.cpp b/src/room.cpp index 9eae941d..f4f1e16f 100644 --- a/src/room.cpp +++ b/src/room.cpp @@ -233,6 +233,11 @@ Room::remoteParticipants() const { return out; } +ConnectionState Room::connectionState() const { + const std::scoped_lock g(lock_); + return connection_state_; +} + E2EEManager *Room::e2eeManager() const { const std::scoped_lock g(lock_); return e2ee_manager_.get(); diff --git a/src/tests/unit/test_result.cpp b/src/tests/unit/test_result.cpp new file mode 100644 index 00000000..d39a6e93 --- /dev/null +++ b/src/tests/unit/test_result.cpp @@ -0,0 +1,213 @@ +/* + * Copyright 2026 LiveKit + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/// @file test_result.cpp +/// @brief Unit tests for the Result and Result types. +/// +/// Covers the invariants documented in result.h: +/// - ok() / has_error() / bool conversion correctness +/// - value() and error() accessor semantics for lvalue, rvalue, and const +/// overloads +/// - Move construction and forwarding behaviour +/// - void specialization + +#include +#include + +#include +#include +#include + +namespace livekit { + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +struct SimpleError { + int code{0}; + std::string message; +}; + +// --------------------------------------------------------------------------- +// Result — success path +// --------------------------------------------------------------------------- + +TEST(ResultTest, SuccessOkIsTrue) { + auto r = Result::success(42); + EXPECT_TRUE(r.ok()); +} + +TEST(ResultTest, SuccessHasErrorIsFalse) { + auto r = Result::success(42); + EXPECT_FALSE(r.has_error()); +} + +TEST(ResultTest, SuccessBoolConversionIsTrue) { + auto r = Result::success(42); + EXPECT_TRUE(static_cast(r)); +} + +TEST(ResultTest, SuccessValueMatchesInput) { + auto r = Result::success(99); + EXPECT_EQ(r.value(), 99); +} + +TEST(ResultTest, SuccessConstValueMatchesInput) { + const auto r = Result::success(7); + EXPECT_EQ(r.value(), 7); +} + +TEST(ResultTest, SuccessValueCanBeMutated) { + auto r = Result::success(1); + r.value() = 100; + EXPECT_EQ(r.value(), 100); +} + +TEST(ResultTest, SuccessStringValue) { + auto r = Result::success("hello"); + EXPECT_EQ(r.value(), "hello"); +} + +TEST(ResultTest, SuccessMoveValueTransfersOwnership) { + auto r = Result, SimpleError>::success( + std::make_unique(55)); + auto ptr = std::move(r).value(); + ASSERT_NE(ptr, nullptr); + EXPECT_EQ(*ptr, 55); +} + +// --------------------------------------------------------------------------- +// Result — failure path +// --------------------------------------------------------------------------- + +TEST(ResultTest, FailureOkIsFalse) { + auto r = Result::failure(SimpleError{1, "oops"}); + EXPECT_FALSE(r.ok()); +} + +TEST(ResultTest, FailureHasErrorIsTrue) { + auto r = Result::failure(SimpleError{1, "oops"}); + EXPECT_TRUE(r.has_error()); +} + +TEST(ResultTest, FailureBoolConversionIsFalse) { + auto r = Result::failure(SimpleError{1, "oops"}); + EXPECT_FALSE(static_cast(r)); +} + +TEST(ResultTest, FailureErrorCodeMatchesInput) { + auto r = Result::failure(SimpleError{42, "bad"}); + EXPECT_EQ(r.error().code, 42); + EXPECT_EQ(r.error().message, "bad"); +} + +TEST(ResultTest, FailureConstErrorMatchesInput) { + const auto r = Result::failure(SimpleError{3, "err"}); + EXPECT_EQ(r.error().code, 3); +} + +TEST(ResultTest, FailureMoveErrorTransfersOwnership) { + auto r = Result>::failure( + std::make_unique(SimpleError{9, "moved"})); + auto err = std::move(r).error(); + ASSERT_NE(err, nullptr); + EXPECT_EQ(err->code, 9); +} + +TEST(ResultTest, FailureStringError) { + auto r = Result::failure("something went wrong"); + EXPECT_EQ(r.error(), "something went wrong"); +} + +// --------------------------------------------------------------------------- +// Result — success path +// --------------------------------------------------------------------------- + +TEST(ResultVoidTest, SuccessOkIsTrue) { + auto r = Result::success(); + EXPECT_TRUE(r.ok()); +} + +TEST(ResultVoidTest, SuccessHasErrorIsFalse) { + auto r = Result::success(); + EXPECT_FALSE(r.has_error()); +} + +TEST(ResultVoidTest, SuccessBoolConversionIsTrue) { + auto r = Result::success(); + EXPECT_TRUE(static_cast(r)); +} + +TEST(ResultVoidTest, SuccessValueIsCallable) { + auto r = Result::success(); + EXPECT_NO_THROW(r.value()); +} + +// --------------------------------------------------------------------------- +// Result — failure path +// --------------------------------------------------------------------------- + +TEST(ResultVoidTest, FailureOkIsFalse) { + auto r = Result::failure(SimpleError{5, "void fail"}); + EXPECT_FALSE(r.ok()); +} + +TEST(ResultVoidTest, FailureHasErrorIsTrue) { + auto r = Result::failure(SimpleError{5, "void fail"}); + EXPECT_TRUE(r.has_error()); +} + +TEST(ResultVoidTest, FailureBoolConversionIsFalse) { + auto r = Result::failure(SimpleError{5, "void fail"}); + EXPECT_FALSE(static_cast(r)); +} + +TEST(ResultVoidTest, FailureErrorMatchesInput) { + auto r = Result::failure(SimpleError{7, "nope"}); + EXPECT_EQ(r.error().code, 7); + EXPECT_EQ(r.error().message, "nope"); +} + +TEST(ResultVoidTest, FailureMoveError) { + auto r = Result::failure("void error"); + auto msg = std::move(r).error(); + EXPECT_EQ(msg, "void error"); +} + +// --------------------------------------------------------------------------- +// if-result idiom +// --------------------------------------------------------------------------- + +TEST(ResultTest, IfResultIdiomSuccessEntersBranch) { + auto r = Result::success(1); + bool entered = false; + if (r) { + entered = true; + } + EXPECT_TRUE(entered); +} + +TEST(ResultTest, IfResultIdiomFailureSkipsBranch) { + auto r = Result::failure(SimpleError{}); + bool entered = false; + if (r) { + entered = true; + } + EXPECT_FALSE(entered); +} + +} // namespace livekit diff --git a/src/tests/unit/test_room_callbacks.cpp b/src/tests/unit/test_room_callbacks.cpp index 90ac35b4..9355a40a 100644 --- a/src/tests/unit/test_room_callbacks.cpp +++ b/src/tests/unit/test_room_callbacks.cpp @@ -267,4 +267,51 @@ TEST_F(RoomCallbackTest, ManyDistinctAudioCallbacksCanBeRegisteredAndCleared) { } } +TEST_F(RoomCallbackTest, DefaultConnectionStateIsDisconnected) { + Room room; + EXPECT_EQ(room.connectionState(), ConnectionState::Disconnected); +} + +TEST_F(RoomCallbackTest, ConnectionStateRemainsDisconnectedWithoutConnect) { + // Register callbacks, do other operations — state must stay Disconnected. + Room room; + room.setOnAudioFrameCallback("alice", TrackSource::SOURCE_MICROPHONE, + [](const AudioFrame &) {}); + room.setOnVideoFrameCallback("alice", TrackSource::SOURCE_CAMERA, + [](const VideoFrame &, std::int64_t) {}); + room.addOnDataFrameCallback( + "alice", "track", + [](const std::vector &, std::optional) {}); + room.registerTextStreamHandler("topic", + [](std::shared_ptr, + const std::string &) {}); + EXPECT_EQ(room.connectionState(), ConnectionState::Disconnected); +} + +TEST_F(RoomCallbackTest, ConnectionStateIsQueryableFromMultipleThreads) { + Room room; + constexpr int kThreads = 8; + constexpr int kIterations = 200; + + std::vector threads; + threads.reserve(kThreads); + std::atomic disconnected_count{0}; + + for (int t = 0; t < kThreads; ++t) { + threads.emplace_back([&room, &disconnected_count, kIterations]() { + for (int i = 0; i < kIterations; ++i) { + if (room.connectionState() == ConnectionState::Disconnected) { + disconnected_count.fetch_add(1, std::memory_order_relaxed); + } + } + }); + } + + for (auto &thread : threads) { + thread.join(); + } + + EXPECT_EQ(disconnected_count.load(), kThreads * kIterations); +} + } // namespace livekit