Skip to content

Commit df8319d

Browse files
pre-encoded ingest through ffi (#260)
1 parent bb343da commit df8319d

16 files changed

Lines changed: 581 additions & 4 deletions

‎CMakeLists.txt‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,7 @@ add_library(livekit SHARED
381381
src/data_track_schema.cpp
382382
src/data_track_stream.cpp
383383
src/e2ee.cpp
384+
src/encoded_video_source.cpp
384385
src/ffi_handle.cpp
385386
src/ffi_client.cpp
386387
src/ffi_client.h

‎client-sdk-rust‎

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/*
2+
* Copyright 2026 LiveKit
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an “AS IS” BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
#pragma once
18+
19+
#include <cstdint>
20+
#include <optional>
21+
#include <vector>
22+
23+
#include "livekit/video_codec.h"
24+
#include "livekit/video_source.h"
25+
#include "livekit/visibility.h"
26+
27+
namespace livekit {
28+
29+
/// @brief Video source for publishing pre-encoded access units without re-encoding.
30+
///
31+
/// Capture calls are synchronous and copy the payload before returning. This
32+
/// type is not safe for concurrent capture calls. Feedback polling can run on
33+
/// another application thread.
34+
class LIVEKIT_API EncodedVideoSource final : public VideoSource {
35+
public:
36+
/// @brief One complete pre-encoded video access unit.
37+
struct Frame {
38+
/// True if this access unit is independently decodable.
39+
bool is_keyframe = false;
40+
/// Encoded frame width in pixels.
41+
/// Set both width and height to zero to use the source resolution.
42+
std::uint32_t width = 0;
43+
/// Encoded frame height in pixels.
44+
/// Set both width and height to zero to use the source resolution.
45+
std::uint32_t height = 0;
46+
/// Capture timestamp in microseconds. Set to zero to use the current time.
47+
std::int64_t timestamp_us = 0;
48+
/// Complete encoded access-unit payload.
49+
std::vector<std::uint8_t> data;
50+
/// Optional packet-trailer metadata.
51+
std::optional<VideoFrameMetadata> metadata;
52+
};
53+
54+
/// @brief Latest encoder rate-control target requested by the publishing pipeline.
55+
struct RateControl {
56+
/// Requested target bitrate in bits per second.
57+
std::uint64_t target_bitrate_bps = 0;
58+
/// Requested frame rate in frames per second.
59+
double framerate_fps = 0.0;
60+
};
61+
62+
/// @brief Pending feedback from the pre-encoded passthrough encoder.
63+
struct Feedback {
64+
/// True when the upstream encoder must produce a key frame.
65+
bool keyframe_requested = false;
66+
/// Latest rate-control target, if the publishing pipeline requested one.
67+
std::optional<RateControl> rate_control;
68+
};
69+
70+
/// @brief Create a pre-encoded source for one codec and initial resolution.
71+
/// @param codec Codec carried by every frame submitted to this source. This
72+
/// must match TrackPublishOptions::video_codec.
73+
/// @param width Initial source width in pixels. Must be in [1, 65535].
74+
/// @param height Initial source height in pixels. Must be in [1, 65535].
75+
/// @throws std::invalid_argument if either dimension is outside [1, 65535].
76+
/// @throws std::runtime_error if source creation fails.
77+
EncodedVideoSource(VideoCodec codec, int width, int height);
78+
79+
/// @brief Codec carried by every frame submitted to this source.
80+
VideoCodec codec() const noexcept { return codec_; }
81+
82+
/// @brief Submit one complete encoded access unit.
83+
/// @param frame Encoded frame. Its payload is copied during this call.
84+
/// @return True if the frame was accepted.
85+
/// @throws std::invalid_argument if the frame is empty, too large, has only
86+
/// one zero dimension, or has a dimension above 65535.
87+
/// @throws std::runtime_error if the FFI request fails.
88+
[[nodiscard]] bool captureFrame(const Frame& frame) const;
89+
90+
/// @brief Consume pending keyframe and rate-control feedback.
91+
/// @return Feedback accumulated since the previous call. Rate-control
92+
/// updates use latest-value-wins semantics.
93+
/// @throws std::runtime_error if the FFI request fails.
94+
[[nodiscard]] Feedback takeFeedback() const;
95+
96+
private:
97+
using VideoSource::captureFrame;
98+
99+
VideoCodec codec_;
100+
};
101+
102+
} // namespace livekit

‎include/livekit/livekit.h‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
#include "livekit/audio_stream.h"
2323
#include "livekit/build.h"
2424
#include "livekit/e2ee.h"
25+
#include "livekit/encoded_video_source.h"
2526
#include "livekit/local_audio_track.h"
2627
#include "livekit/local_participant.h"
2728
#include "livekit/local_track_publication.h"
@@ -37,6 +38,7 @@
3738
#include "livekit/token_source.h"
3839
#include "livekit/tracing.h"
3940
#include "livekit/track_publication.h"
41+
#include "livekit/video_codec.h"
4042
#include "livekit/video_frame.h"
4143
#include "livekit/video_source.h"
4244
#include "livekit/video_stream.h"

‎include/livekit/local_participant.h‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,10 @@ class LIVEKIT_API LocalParticipant : public Participant {
146146
///
147147
/// The caller retains ownership of @p source and should use it directly
148148
/// for frame capture on the video thread.
149+
///
150+
/// If @p source is an @ref EncodedVideoSource, the track is published with
151+
/// @ref VideoEncoderBackend::PreEncoded and the source's codec. Use
152+
/// @ref publishTrack directly to control any other publish option.
149153
std::shared_ptr<LocalVideoTrack> publishVideoTrack(const std::string& name,
150154
const std::shared_ptr<VideoSource>& source,
151155
TrackSource track_source);

‎include/livekit/room_event_types.h‎

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
#include <string>
2424
#include <vector>
2525

26+
#include "livekit/video_codec.h"
27+
2628
namespace livekit {
2729

2830
// Forward declarations to avoid pulling in heavy headers.
@@ -34,7 +36,6 @@ class LocalTrackPublication;
3436
class RemoteTrackPublication;
3537
class TrackPublication;
3638

37-
enum class VideoCodec;
3839
enum class TrackSource;
3940

4041
/// Overall quality of a participant's connection.
@@ -291,6 +292,24 @@ struct AudioEncodingOptions {
291292
std::uint64_t max_bitrate = 0;
292293
};
293294

295+
/// @brief Preferred encoder backend for a published video track.
296+
enum class VideoEncoderBackend {
297+
/// Use the SDK's default encoder selection.
298+
Auto = 0,
299+
/// Prefer a software encoder.
300+
Software = 1,
301+
/// Prefer any available hardware encoder.
302+
Hardware = 2,
303+
/// Prefer NVIDIA NVENC.
304+
Nvenc = 3,
305+
/// Prefer VAAPI.
306+
Vaapi = 4,
307+
/// Prefer VideoToolbox on Apple platforms.
308+
VideoToolbox = 5,
309+
/// Pass pre-encoded access units through without encoding them again.
310+
PreEncoded = 6,
311+
};
312+
294313
/// @brief Controls how the encoder degrades quality when bandwidth is constrained.
295314
enum class DegradationPreference {
296315
/// Balance between framerate and resolution degradation.
@@ -359,6 +378,10 @@ struct TrackPublishOptions {
359378
/// Controls how the encoder trades off between resolution and framerate
360379
/// when bandwidth is constrained. If not set, the server defaults apply.
361380
std::optional<DegradationPreference> degradation_preference;
381+
382+
/// Optional video encoder backend. Pre-encoded sources must select
383+
/// @ref VideoEncoderBackend::PreEncoded.
384+
std::optional<VideoEncoderBackend> video_encoder;
362385
};
363386

364387
// ---------------------------------------------------------

‎include/livekit/video_codec.h‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/*
2+
* Copyright 2026 LiveKit
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
#pragma once
18+
19+
namespace livekit {
20+
21+
/// @brief Codec used to publish a video track.
22+
enum class VideoCodec { VP8 = 0, H264 = 1, AV1 = 2, VP9 = 3, H265 = 4 };
23+
24+
} // namespace livekit

‎include/livekit/video_source.h‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
namespace livekit {
2727

2828
class VideoFrame;
29+
class EncodedVideoSource;
2930

3031
/// Rotation of a video frame.
3132
///
@@ -95,6 +96,11 @@ class LIVEKIT_API VideoSource {
9596
VideoRotation rotation = VideoRotation::VIDEO_ROTATION_0);
9697

9798
private:
99+
friend class EncodedVideoSource;
100+
101+
enum class SourceType { Native, Encoded };
102+
VideoSource(int width, int height, SourceType source_type);
103+
98104
FfiHandle handle_; // owned FFI handle
99105
int width_{0};
100106
int height_{0};

‎src/encoded_video_source.cpp‎

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
/*
2+
* Copyright 2026 LiveKit
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an “AS IS” BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
#include "livekit/encoded_video_source.h"
18+
19+
#include <cstddef>
20+
#include <stdexcept>
21+
22+
#include "ffi.pb.h"
23+
#include "ffi_client.h"
24+
#include "video_frame.pb.h"
25+
#include "video_utils.h"
26+
27+
namespace livekit {
28+
namespace {
29+
30+
constexpr std::size_t kMaxFrameSize = std::size_t{64} * 1024U * 1024U;
31+
constexpr std::uint32_t kMaxFrameDimension = 65535U;
32+
33+
int validateDimension(int dimension) {
34+
if (dimension <= 0 || static_cast<std::uint32_t>(dimension) > kMaxFrameDimension) {
35+
throw std::invalid_argument("EncodedVideoSource: dimensions must be positive and at most 65535");
36+
}
37+
return dimension;
38+
}
39+
40+
proto::VideoCodec toProtoCodec(VideoCodec codec) {
41+
switch (codec) {
42+
case VideoCodec::H264:
43+
return proto::VideoCodec::H264;
44+
case VideoCodec::H265:
45+
return proto::VideoCodec::H265;
46+
case VideoCodec::VP8:
47+
return proto::VideoCodec::VP8;
48+
case VideoCodec::VP9:
49+
return proto::VideoCodec::VP9;
50+
case VideoCodec::AV1:
51+
return proto::VideoCodec::AV1;
52+
}
53+
throw std::invalid_argument("EncodedVideoSource: unknown codec");
54+
}
55+
56+
} // namespace
57+
58+
EncodedVideoSource::EncodedVideoSource(VideoCodec codec, int width, int height)
59+
: VideoSource(validateDimension(width), validateDimension(height), VideoSource::SourceType::Encoded),
60+
codec_(codec) {}
61+
62+
bool EncodedVideoSource::captureFrame(const Frame& frame) const {
63+
if ((frame.width == 0) != (frame.height == 0)) {
64+
throw std::invalid_argument("EncodedVideoSource: frame dimensions must both be zero or both be non-zero");
65+
}
66+
if (frame.width > kMaxFrameDimension || frame.height > kMaxFrameDimension) {
67+
throw std::invalid_argument("EncodedVideoSource: frame dimensions must not exceed 65535");
68+
}
69+
if (frame.data.empty() || frame.data.size() > kMaxFrameSize) {
70+
throw std::invalid_argument("EncodedVideoSource: frame payload must be between 1 byte and 64 MiB");
71+
}
72+
if (ffiHandleId() == 0) {
73+
throw std::runtime_error("EncodedVideoSource: invalid FFI handle");
74+
}
75+
76+
proto::FfiRequest req;
77+
auto* msg = req.mutable_capture_encoded_video_frame();
78+
msg->set_source_handle(ffiHandleId());
79+
auto* buffer = msg->mutable_buffer();
80+
buffer->set_data_ptr(reinterpret_cast<std::uintptr_t>(frame.data.data()));
81+
buffer->set_data_len(frame.data.size());
82+
msg->set_codec(toProtoCodec(codec_));
83+
msg->set_frame_type(frame.is_keyframe ? proto::EncodedFrameType::ENCODED_FRAME_KEY
84+
: proto::EncodedFrameType::ENCODED_FRAME_DELTA);
85+
msg->set_width(frame.width == 0 ? static_cast<std::uint32_t>(width()) : frame.width);
86+
msg->set_height(frame.height == 0 ? static_cast<std::uint32_t>(height()) : frame.height);
87+
msg->set_timestamp_us(frame.timestamp_us);
88+
if (auto metadata = toProto(frame.metadata)) {
89+
msg->mutable_metadata()->CopyFrom(*metadata);
90+
}
91+
92+
const proto::FfiResponse resp = FfiClient::instance().sendRequest(req);
93+
if (!resp.has_capture_encoded_video_frame()) {
94+
throw std::runtime_error("EncodedVideoSource: missing capture response");
95+
}
96+
return resp.capture_encoded_video_frame().accepted();
97+
}
98+
99+
EncodedVideoSource::Feedback EncodedVideoSource::takeFeedback() const {
100+
if (ffiHandleId() == 0) {
101+
throw std::runtime_error("EncodedVideoSource: invalid FFI handle");
102+
}
103+
104+
proto::FfiRequest req;
105+
req.mutable_take_encoded_video_source_feedback()->set_source_handle(ffiHandleId());
106+
const proto::FfiResponse resp = FfiClient::instance().sendRequest(req);
107+
if (!resp.has_take_encoded_video_source_feedback()) {
108+
throw std::runtime_error("EncodedVideoSource: missing feedback response");
109+
}
110+
111+
const auto& proto_feedback = resp.take_encoded_video_source_feedback();
112+
Feedback feedback;
113+
feedback.keyframe_requested = proto_feedback.keyframe_requested();
114+
if (proto_feedback.has_rate_control()) {
115+
feedback.rate_control =
116+
RateControl{proto_feedback.rate_control().target_bitrate_bps(), proto_feedback.rate_control().framerate_fps()};
117+
}
118+
return feedback;
119+
}
120+
121+
} // namespace livekit

‎src/local_participant.cpp‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,13 @@
1717
#include "livekit/local_participant.h"
1818

1919
#include <chrono>
20+
#include <memory>
2021
#include <stdexcept>
2122

2223
#include "data_track.pb.h"
2324
#include "ffi.pb.h"
2425
#include "ffi_client.h"
26+
#include "livekit/encoded_video_source.h"
2527
#include "livekit/ffi_handle.h"
2628
#include "livekit/local_audio_track.h"
2729
#include "livekit/local_data_track.h"
@@ -209,6 +211,14 @@ std::shared_ptr<LocalVideoTrack> LocalParticipant::publishVideoTrack(const std::
209211
auto track = LocalVideoTrack::createLocalVideoTrack(name, source);
210212
TrackPublishOptions opts;
211213
opts.source = track_source;
214+
// A pre-encoded source cannot be re-encoded, so the passthrough backend and
215+
// the source's codec are the only valid choices here.
216+
if (const auto encoded = std::dynamic_pointer_cast<EncodedVideoSource>(source)) {
217+
opts.video_encoder = VideoEncoderBackend::PreEncoded;
218+
opts.video_codec = encoded->codec();
219+
// A single-stream encoded source cannot supply the requested layers, so turn off simulcast.
220+
opts.simulcast = false;
221+
}
212222
publishTrack(track, opts);
213223
return track;
214224
}

0 commit comments

Comments
 (0)