diff --git a/CMakeLists.txt b/CMakeLists.txt index 1c7b244c..c9347c62 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -381,6 +381,7 @@ add_library(livekit SHARED src/data_track_schema.cpp src/data_track_stream.cpp src/e2ee.cpp + src/encoded_video_source.cpp src/ffi_handle.cpp src/ffi_client.cpp src/ffi_client.h diff --git a/client-sdk-rust b/client-sdk-rust index 8066415f..bae4df2f 160000 --- a/client-sdk-rust +++ b/client-sdk-rust @@ -1 +1 @@ -Subproject commit 8066415f8faa09a0ec0a6643e6cfdaa825167c65 +Subproject commit bae4df2f3d6c8f0f6ce81a7370c445779f1948d2 diff --git a/include/livekit/encoded_video_source.h b/include/livekit/encoded_video_source.h new file mode 100644 index 00000000..3def1572 --- /dev/null +++ b/include/livekit/encoded_video_source.h @@ -0,0 +1,102 @@ +/* + * Copyright 2026 LiveKit + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an “AS IS” BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include + +#include "livekit/video_codec.h" +#include "livekit/video_source.h" +#include "livekit/visibility.h" + +namespace livekit { + +/// @brief Video source for publishing pre-encoded access units without re-encoding. +/// +/// Capture calls are synchronous and copy the payload before returning. This +/// type is not safe for concurrent capture calls. Feedback polling can run on +/// another application thread. +class LIVEKIT_API EncodedVideoSource final : public VideoSource { +public: + /// @brief One complete pre-encoded video access unit. + struct Frame { + /// True if this access unit is independently decodable. + bool is_keyframe = false; + /// Encoded frame width in pixels. + /// Set both width and height to zero to use the source resolution. + std::uint32_t width = 0; + /// Encoded frame height in pixels. + /// Set both width and height to zero to use the source resolution. + std::uint32_t height = 0; + /// Capture timestamp in microseconds. Set to zero to use the current time. + std::int64_t timestamp_us = 0; + /// Complete encoded access-unit payload. + std::vector data; + /// Optional packet-trailer metadata. + std::optional metadata; + }; + + /// @brief Latest encoder rate-control target requested by the publishing pipeline. + struct RateControl { + /// Requested target bitrate in bits per second. + std::uint64_t target_bitrate_bps = 0; + /// Requested frame rate in frames per second. + double framerate_fps = 0.0; + }; + + /// @brief Pending feedback from the pre-encoded passthrough encoder. + struct Feedback { + /// True when the upstream encoder must produce a key frame. + bool keyframe_requested = false; + /// Latest rate-control target, if the publishing pipeline requested one. + std::optional rate_control; + }; + + /// @brief Create a pre-encoded source for one codec and initial resolution. + /// @param codec Codec carried by every frame submitted to this source. This + /// must match TrackPublishOptions::video_codec. + /// @param width Initial source width in pixels. Must be in [1, 65535]. + /// @param height Initial source height in pixels. Must be in [1, 65535]. + /// @throws std::invalid_argument if either dimension is outside [1, 65535]. + /// @throws std::runtime_error if source creation fails. + EncodedVideoSource(VideoCodec codec, int width, int height); + + /// @brief Codec carried by every frame submitted to this source. + VideoCodec codec() const noexcept { return codec_; } + + /// @brief Submit one complete encoded access unit. + /// @param frame Encoded frame. Its payload is copied during this call. + /// @return True if the frame was accepted. + /// @throws std::invalid_argument if the frame is empty, too large, has only + /// one zero dimension, or has a dimension above 65535. + /// @throws std::runtime_error if the FFI request fails. + [[nodiscard]] bool captureFrame(const Frame& frame) const; + + /// @brief Consume pending keyframe and rate-control feedback. + /// @return Feedback accumulated since the previous call. Rate-control + /// updates use latest-value-wins semantics. + /// @throws std::runtime_error if the FFI request fails. + [[nodiscard]] Feedback takeFeedback() const; + +private: + using VideoSource::captureFrame; + + VideoCodec codec_; +}; + +} // namespace livekit diff --git a/include/livekit/livekit.h b/include/livekit/livekit.h index d0aaee89..9b22b7e9 100644 --- a/include/livekit/livekit.h +++ b/include/livekit/livekit.h @@ -22,6 +22,7 @@ #include "livekit/audio_stream.h" #include "livekit/build.h" #include "livekit/e2ee.h" +#include "livekit/encoded_video_source.h" #include "livekit/local_audio_track.h" #include "livekit/local_participant.h" #include "livekit/local_track_publication.h" @@ -37,6 +38,7 @@ #include "livekit/token_source.h" #include "livekit/tracing.h" #include "livekit/track_publication.h" +#include "livekit/video_codec.h" #include "livekit/video_frame.h" #include "livekit/video_source.h" #include "livekit/video_stream.h" diff --git a/include/livekit/local_participant.h b/include/livekit/local_participant.h index 91d701ad..28ba83ef 100644 --- a/include/livekit/local_participant.h +++ b/include/livekit/local_participant.h @@ -146,6 +146,10 @@ class LIVEKIT_API LocalParticipant : public Participant { /// /// The caller retains ownership of @p source and should use it directly /// for frame capture on the video thread. + /// + /// If @p source is an @ref EncodedVideoSource, the track is published with + /// @ref VideoEncoderBackend::PreEncoded and the source's codec. Use + /// @ref publishTrack directly to control any other publish option. std::shared_ptr publishVideoTrack(const std::string& name, const std::shared_ptr& source, TrackSource track_source); diff --git a/include/livekit/room_event_types.h b/include/livekit/room_event_types.h index 86589681..10fdffce 100644 --- a/include/livekit/room_event_types.h +++ b/include/livekit/room_event_types.h @@ -23,6 +23,8 @@ #include #include +#include "livekit/video_codec.h" + namespace livekit { // Forward declarations to avoid pulling in heavy headers. @@ -34,7 +36,6 @@ class LocalTrackPublication; class RemoteTrackPublication; class TrackPublication; -enum class VideoCodec; enum class TrackSource; /// Overall quality of a participant's connection. @@ -291,6 +292,24 @@ struct AudioEncodingOptions { std::uint64_t max_bitrate = 0; }; +/// @brief Preferred encoder backend for a published video track. +enum class VideoEncoderBackend { + /// Use the SDK's default encoder selection. + Auto = 0, + /// Prefer a software encoder. + Software = 1, + /// Prefer any available hardware encoder. + Hardware = 2, + /// Prefer NVIDIA NVENC. + Nvenc = 3, + /// Prefer VAAPI. + Vaapi = 4, + /// Prefer VideoToolbox on Apple platforms. + VideoToolbox = 5, + /// Pass pre-encoded access units through without encoding them again. + PreEncoded = 6, +}; + /// @brief Controls how the encoder degrades quality when bandwidth is constrained. enum class DegradationPreference { /// Balance between framerate and resolution degradation. @@ -359,6 +378,10 @@ struct TrackPublishOptions { /// Controls how the encoder trades off between resolution and framerate /// when bandwidth is constrained. If not set, the server defaults apply. std::optional degradation_preference; + + /// Optional video encoder backend. Pre-encoded sources must select + /// @ref VideoEncoderBackend::PreEncoded. + std::optional video_encoder; }; // --------------------------------------------------------- diff --git a/include/livekit/video_codec.h b/include/livekit/video_codec.h new file mode 100644 index 00000000..0cbfc90f --- /dev/null +++ b/include/livekit/video_codec.h @@ -0,0 +1,24 @@ +/* + * Copyright 2026 LiveKit + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +namespace livekit { + +/// @brief Codec used to publish a video track. +enum class VideoCodec { VP8 = 0, H264 = 1, AV1 = 2, VP9 = 3, H265 = 4 }; + +} // namespace livekit diff --git a/include/livekit/video_source.h b/include/livekit/video_source.h index 44e69d8f..0c472d8a 100644 --- a/include/livekit/video_source.h +++ b/include/livekit/video_source.h @@ -26,6 +26,7 @@ namespace livekit { class VideoFrame; +class EncodedVideoSource; /// Rotation of a video frame. /// @@ -95,6 +96,11 @@ class LIVEKIT_API VideoSource { VideoRotation rotation = VideoRotation::VIDEO_ROTATION_0); private: + friend class EncodedVideoSource; + + enum class SourceType { Native, Encoded }; + VideoSource(int width, int height, SourceType source_type); + FfiHandle handle_; // owned FFI handle int width_{0}; int height_{0}; diff --git a/src/encoded_video_source.cpp b/src/encoded_video_source.cpp new file mode 100644 index 00000000..581a9a75 --- /dev/null +++ b/src/encoded_video_source.cpp @@ -0,0 +1,121 @@ +/* + * 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. + */ + +#include "livekit/encoded_video_source.h" + +#include +#include + +#include "ffi.pb.h" +#include "ffi_client.h" +#include "video_frame.pb.h" +#include "video_utils.h" + +namespace livekit { +namespace { + +constexpr std::size_t kMaxFrameSize = std::size_t{64} * 1024U * 1024U; +constexpr std::uint32_t kMaxFrameDimension = 65535U; + +int validateDimension(int dimension) { + if (dimension <= 0 || static_cast(dimension) > kMaxFrameDimension) { + throw std::invalid_argument("EncodedVideoSource: dimensions must be positive and at most 65535"); + } + return dimension; +} + +proto::VideoCodec toProtoCodec(VideoCodec codec) { + switch (codec) { + case VideoCodec::H264: + return proto::VideoCodec::H264; + case VideoCodec::H265: + return proto::VideoCodec::H265; + case VideoCodec::VP8: + return proto::VideoCodec::VP8; + case VideoCodec::VP9: + return proto::VideoCodec::VP9; + case VideoCodec::AV1: + return proto::VideoCodec::AV1; + } + throw std::invalid_argument("EncodedVideoSource: unknown codec"); +} + +} // namespace + +EncodedVideoSource::EncodedVideoSource(VideoCodec codec, int width, int height) + : VideoSource(validateDimension(width), validateDimension(height), VideoSource::SourceType::Encoded), + codec_(codec) {} + +bool EncodedVideoSource::captureFrame(const Frame& frame) const { + if ((frame.width == 0) != (frame.height == 0)) { + throw std::invalid_argument("EncodedVideoSource: frame dimensions must both be zero or both be non-zero"); + } + if (frame.width > kMaxFrameDimension || frame.height > kMaxFrameDimension) { + throw std::invalid_argument("EncodedVideoSource: frame dimensions must not exceed 65535"); + } + if (frame.data.empty() || frame.data.size() > kMaxFrameSize) { + throw std::invalid_argument("EncodedVideoSource: frame payload must be between 1 byte and 64 MiB"); + } + if (ffiHandleId() == 0) { + throw std::runtime_error("EncodedVideoSource: invalid FFI handle"); + } + + proto::FfiRequest req; + auto* msg = req.mutable_capture_encoded_video_frame(); + msg->set_source_handle(ffiHandleId()); + auto* buffer = msg->mutable_buffer(); + buffer->set_data_ptr(reinterpret_cast(frame.data.data())); + buffer->set_data_len(frame.data.size()); + msg->set_codec(toProtoCodec(codec_)); + msg->set_frame_type(frame.is_keyframe ? proto::EncodedFrameType::ENCODED_FRAME_KEY + : proto::EncodedFrameType::ENCODED_FRAME_DELTA); + msg->set_width(frame.width == 0 ? static_cast(width()) : frame.width); + msg->set_height(frame.height == 0 ? static_cast(height()) : frame.height); + msg->set_timestamp_us(frame.timestamp_us); + if (auto metadata = toProto(frame.metadata)) { + msg->mutable_metadata()->CopyFrom(*metadata); + } + + const proto::FfiResponse resp = FfiClient::instance().sendRequest(req); + if (!resp.has_capture_encoded_video_frame()) { + throw std::runtime_error("EncodedVideoSource: missing capture response"); + } + return resp.capture_encoded_video_frame().accepted(); +} + +EncodedVideoSource::Feedback EncodedVideoSource::takeFeedback() const { + if (ffiHandleId() == 0) { + throw std::runtime_error("EncodedVideoSource: invalid FFI handle"); + } + + proto::FfiRequest req; + req.mutable_take_encoded_video_source_feedback()->set_source_handle(ffiHandleId()); + const proto::FfiResponse resp = FfiClient::instance().sendRequest(req); + if (!resp.has_take_encoded_video_source_feedback()) { + throw std::runtime_error("EncodedVideoSource: missing feedback response"); + } + + const auto& proto_feedback = resp.take_encoded_video_source_feedback(); + Feedback feedback; + feedback.keyframe_requested = proto_feedback.keyframe_requested(); + if (proto_feedback.has_rate_control()) { + feedback.rate_control = + RateControl{proto_feedback.rate_control().target_bitrate_bps(), proto_feedback.rate_control().framerate_fps()}; + } + return feedback; +} + +} // namespace livekit diff --git a/src/local_participant.cpp b/src/local_participant.cpp index 1157aa0e..5203ca83 100644 --- a/src/local_participant.cpp +++ b/src/local_participant.cpp @@ -17,11 +17,13 @@ #include "livekit/local_participant.h" #include +#include #include #include "data_track.pb.h" #include "ffi.pb.h" #include "ffi_client.h" +#include "livekit/encoded_video_source.h" #include "livekit/ffi_handle.h" #include "livekit/local_audio_track.h" #include "livekit/local_data_track.h" @@ -209,6 +211,14 @@ std::shared_ptr LocalParticipant::publishVideoTrack(const std:: auto track = LocalVideoTrack::createLocalVideoTrack(name, source); TrackPublishOptions opts; opts.source = track_source; + // A pre-encoded source cannot be re-encoded, so the passthrough backend and + // the source's codec are the only valid choices here. + if (const auto encoded = std::dynamic_pointer_cast(source)) { + opts.video_encoder = VideoEncoderBackend::PreEncoded; + opts.video_codec = encoded->codec(); + // A single-stream encoded source cannot supply the requested layers, so turn off simulcast. + opts.simulcast = false; + } publishTrack(track, opts); return track; } diff --git a/src/room_proto_converter.cpp b/src/room_proto_converter.cpp index 4c5da0ed..3d543756 100644 --- a/src/room_proto_converter.cpp +++ b/src/room_proto_converter.cpp @@ -512,6 +512,9 @@ proto::TrackPublishOptions toProto(const TrackPublishOptions& in) { if (in.video_codec) { msg.set_video_codec(static_cast(*in.video_codec)); } + if (in.video_encoder) { + msg.set_video_encoder(static_cast(*in.video_encoder)); + } if (in.dtx) { msg.set_dtx(*in.dtx); } @@ -550,6 +553,9 @@ TrackPublishOptions fromProto(const proto::TrackPublishOptions& in) { if (in.has_video_codec()) { out.video_codec = static_cast(in.video_codec()); } + if (in.has_video_encoder()) { + out.video_encoder = static_cast(in.video_encoder()); + } if (in.has_dtx()) { out.dtx = in.dtx(); } diff --git a/src/tests/integration/test_encoded_video_ingestion.cpp b/src/tests/integration/test_encoded_video_ingestion.cpp new file mode 100644 index 00000000..dbe4cd09 --- /dev/null +++ b/src/tests/integration/test_encoded_video_ingestion.cpp @@ -0,0 +1,116 @@ +/* + * 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. + */ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "tests/common/test_common.h" + +namespace livekit::test { +namespace { + +// One 16x16 baseline-profile H.264 Annex-B key access unit. It contains AUD, +// SPS, PPS, and IDR NAL units and was generated from a black I420 frame. +constexpr std::array kH264KeyAccessUnit = { + 0x00, 0x00, 0x00, 0x01, 0x09, 0x10, 0x00, 0x00, 0x00, 0x01, 0x67, 0x42, 0xc0, 0x0a, 0xdd, + 0xe8, 0x40, 0x00, 0x00, 0x03, 0x00, 0x40, 0x00, 0x00, 0x05, 0x23, 0xc4, 0x89, 0xe0, 0x00, + 0x00, 0x00, 0x01, 0x68, 0xce, 0x0f, 0xc8, 0x00, 0x00, 0x01, 0x65, 0x88, 0x84, 0x3a, 0x11, + 0x8a, 0x00, 0x02, 0x4a, 0xb1, 0xc0, 0x00, 0x44, 0x66, 0x38, 0x00, 0x08, 0x8c, 0xe0}; + +} // namespace + +class EncodedVideoIngestionIntegrationTest : public LiveKitTestBase {}; + +TEST_F(EncodedVideoIngestionIntegrationTest, PublisherSendsPreEncodedVideoToReceiver) { + failIfNotConfigured(); + + Room sender_room; + Room receiver_room; + const RoomOptions room_options; + ASSERT_TRUE(receiver_room.connect(config_.url, config_.token_b, room_options)); + ASSERT_TRUE(sender_room.connect(config_.url, config_.token_a, room_options)); + + const std::string sender_identity = lockLocalParticipant(sender_room)->identity(); + ASSERT_TRUE(waitForParticipant(&receiver_room, sender_identity, 10s)); + + std::mutex mutex; + std::condition_variable cv; + bool received_frame = false; + constexpr char kTrackName[] = "pre-encoded-h264"; + receiver_room.setOnVideoFrameEventCallback(sender_identity, kTrackName, + [&mutex, &cv, &received_frame](const VideoFrameEvent& event) { + if (event.frame.width() != 16 || event.frame.height() != 16) { + return; + } + { + const std::scoped_lock lock(mutex); + received_frame = true; + } + cv.notify_all(); + }); + + auto source = std::make_shared(VideoCodec::H264, 16, 16); + auto track = LocalVideoTrack::createLocalVideoTrack(kTrackName, source); + TrackPublishOptions publish_options; + publish_options.video_codec = VideoCodec::H264; + publish_options.video_encoder = VideoEncoderBackend::PreEncoded; + publish_options.simulcast = false; + publish_options.source = TrackSource::SOURCE_CAMERA; + ASSERT_NO_THROW(lockLocalParticipant(sender_room)->publishTrack(track, publish_options)); + + std::atomic publishing{true}; + std::thread publisher([&source, &publishing]() { + EncodedVideoSource::Frame frame; + frame.is_keyframe = true; + frame.data.assign(kH264KeyAccessUnit.begin(), kH264KeyAccessUnit.end()); + while (publishing.load(std::memory_order_relaxed)) { + frame.timestamp_us = static_cast(getTimestampUs()); + try { + (void)source->captureFrame(frame); + } catch (...) { + publishing.store(false, std::memory_order_relaxed); + break; + } + std::this_thread::sleep_for(100ms); + } + }); + + bool received = false; + { + std::unique_lock lock(mutex); + received = cv.wait_for(lock, 10s, [&received_frame] { return received_frame; }); + } + + publishing.store(false, std::memory_order_relaxed); + publisher.join(); + receiver_room.clearOnVideoFrameCallback(sender_identity, kTrackName); + if (track->publication()) { + lockLocalParticipant(sender_room)->unpublishTrack(track->publication()->sid()); + } + + EXPECT_TRUE(received) << "Timed out waiting for a decoded pre-encoded H.264 frame"; +} + +} // namespace livekit::test diff --git a/src/tests/stress/test_encoded_video_ingestion_stress.cpp b/src/tests/stress/test_encoded_video_ingestion_stress.cpp new file mode 100644 index 00000000..41ef3d94 --- /dev/null +++ b/src/tests/stress/test_encoded_video_ingestion_stress.cpp @@ -0,0 +1,61 @@ +/* + * 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. + */ + +#include +#include +#include + +#include +#include +#include +#include + +namespace livekit::test { + +class EncodedVideoIngestionStressTest : public ::testing::Test { +protected: + void SetUp() override { livekit::initialize(livekit::LogLevel::Warn); } + void TearDown() override { livekit::shutdown(); } +}; + +TEST_F(EncodedVideoIngestionStressTest, MeasuresSustainedFfiSubmission) { + constexpr std::size_t kPayloadSize = std::size_t{256} * 1024U; + constexpr std::size_t kFrameCount = 300U; + + const EncodedVideoSource source(VideoCodec::H264, 1920, 1080); + EncodedVideoSource::Frame frame; + frame.is_keyframe = true; + frame.data.assign(kPayloadSize, 0x55); + + std::size_t accepted = 0; + const auto started_at = std::chrono::steady_clock::now(); + for (std::size_t index = 0; index < kFrameCount; ++index) { + frame.timestamp_us = static_cast(index) * 33333; + if (source.captureFrame(frame)) { + ++accepted; + } + } + const auto elapsed = std::chrono::steady_clock::now() - started_at; + const double elapsed_seconds = std::chrono::duration(elapsed).count(); + const double throughput_mib_per_second = + (static_cast(kPayloadSize * kFrameCount) / (1024.0 * 1024.0)) / elapsed_seconds; + + std::cout << "Pre-encoded FFI submission: " << throughput_mib_per_second << " MiB/s (" << kFrameCount << " frames)\n"; + EXPECT_EQ(accepted, kFrameCount); + EXPECT_LT(elapsed, std::chrono::seconds(30)); +} + +} // namespace livekit::test diff --git a/src/tests/unit/test_encoded_video_source.cpp b/src/tests/unit/test_encoded_video_source.cpp new file mode 100644 index 00000000..c83713d2 --- /dev/null +++ b/src/tests/unit/test_encoded_video_source.cpp @@ -0,0 +1,85 @@ +/* + * 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. + */ + +#include +#include +#include + +#include +#include +#include + +namespace livekit::test { + +class EncodedVideoSourceTest : public ::testing::Test { +protected: + void SetUp() override { livekit::initialize(livekit::LogLevel::Info); } + void TearDown() override { livekit::shutdown(); } +}; + +TEST_F(EncodedVideoSourceTest, ConstructAndQueryProperties) { + const EncodedVideoSource source(VideoCodec::H264, 640, 480); + EXPECT_EQ(source.width(), 640); + EXPECT_EQ(source.height(), 480); + EXPECT_EQ(source.codec(), VideoCodec::H264); + EXPECT_NE(source.ffiHandleId(), 0U); +} + +TEST_F(EncodedVideoSourceTest, RejectsInvalidDimensions) { + EXPECT_THROW((void)EncodedVideoSource(VideoCodec::H264, 0, 480), std::invalid_argument); + EXPECT_THROW((void)EncodedVideoSource(VideoCodec::H264, 640, -1), std::invalid_argument); + EXPECT_THROW((void)EncodedVideoSource(VideoCodec::H264, 65536, 480), std::invalid_argument); +} + +TEST_F(EncodedVideoSourceTest, RejectsInvalidFramesBeforeFfi) { + const EncodedVideoSource source(VideoCodec::H264, 640, 480); + EncodedVideoSource::Frame frame; + + EXPECT_THROW((void)source.captureFrame(frame), std::invalid_argument); + + frame.data = {0x01}; + frame.width = 640; + EXPECT_THROW((void)source.captureFrame(frame), std::invalid_argument); + + // Dimensions above the signed range the encoder path uses must be rejected + // here rather than wrapping negative downstream. + frame.width = 65536; + frame.height = 480; + EXPECT_THROW((void)source.captureFrame(frame), std::invalid_argument); + + frame.width = 640; + frame.height = std::numeric_limits::max(); + EXPECT_THROW((void)source.captureFrame(frame), std::invalid_argument); +} + +TEST_F(EncodedVideoSourceTest, InitialFeedbackIsEmpty) { + const EncodedVideoSource source(VideoCodec::H264, 640, 480); + const EncodedVideoSource::Feedback feedback = source.takeFeedback(); + EXPECT_FALSE(feedback.keyframe_requested); + EXPECT_FALSE(feedback.rate_control.has_value()); +} + +TEST_F(EncodedVideoSourceTest, SubmitsOwnedAccessUnit) { + const EncodedVideoSource source(VideoCodec::H264, 16, 16); + EncodedVideoSource::Frame frame; + frame.is_keyframe = true; + frame.timestamp_us = 1234; + frame.data = {0x00, 0x00, 0x00, 0x01, 0x65, 0x88}; + + EXPECT_TRUE(source.captureFrame(frame)); +} + +} // namespace livekit::test diff --git a/src/tests/unit/test_video_frame_metadata.cpp b/src/tests/unit/test_video_frame_metadata.cpp index 5c683e84..f4de805e 100644 --- a/src/tests/unit/test_video_frame_metadata.cpp +++ b/src/tests/unit/test_video_frame_metadata.cpp @@ -152,6 +152,19 @@ TEST(TrackPublishOptionsTest, DegradationPreferenceRoundTrip) { EXPECT_EQ(*round_trip.degradation_preference, DegradationPreference::MaintainFramerateAndResolution); } +TEST(TrackPublishOptionsTest, VideoEncoderBackendRoundTrip) { + TrackPublishOptions options; + options.video_encoder = VideoEncoderBackend::PreEncoded; + + const proto::TrackPublishOptions proto_options = toProto(options); + ASSERT_TRUE(proto_options.has_video_encoder()); + EXPECT_EQ(proto_options.video_encoder(), proto::VideoEncoderBackend::ENCODER_BACKEND_PRE_ENCODED); + + const TrackPublishOptions round_trip = fromProto(proto_options); + ASSERT_TRUE(round_trip.video_encoder.has_value()); + EXPECT_EQ(*round_trip.video_encoder, VideoEncoderBackend::PreEncoded); +} + TEST(TrackPublishOptionsTest, DeprecatedPacketTrailerFeaturesAreMerged) { TrackPublishOptions options; diff --git a/src/video_source.cpp b/src/video_source.cpp index e173fe7b..2e280257 100644 --- a/src/video_source.cpp +++ b/src/video_source.cpp @@ -26,10 +26,13 @@ namespace livekit { -VideoSource::VideoSource(int width, int height) : width_(width), height_(height) { +VideoSource::VideoSource(int width, int height) : VideoSource(width, height, SourceType::Native) {} + +VideoSource::VideoSource(int width, int height, SourceType source_type) : width_(width), height_(height) { proto::FfiRequest req; auto* msg = req.mutable_new_video_source(); - msg->set_type(proto::VideoSourceType::VIDEO_SOURCE_NATIVE); + msg->set_type(source_type == SourceType::Encoded ? proto::VideoSourceType::VIDEO_SOURCE_ENCODED + : proto::VideoSourceType::VIDEO_SOURCE_NATIVE); msg->mutable_resolution()->set_width(width_); msg->mutable_resolution()->set_height(height_);