Skip to content

Commit 8069213

Browse files
Bring in VideoFrame FrameMetadata from FFI
1 parent 32c593f commit 8069213

19 files changed

+974
-34
lines changed

examples/CMakeLists.txt

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ set(EXAMPLES_COMMON_DIR ${CMAKE_CURRENT_SOURCE_DIR}/common)
3939
# All example executables (used for copying livekit_ffi DLL/shared lib)
4040
set(EXAMPLES_ALL
4141
SimpleRoom
42+
UserTimestampedVideoProducer
43+
UserTimestampedVideoConsumer
4244
SimpleRpc
4345
SimpleJoystickSender
4446
SimpleJoystickReceiver
@@ -234,6 +236,32 @@ target_link_libraries(SimpleDataStream
234236
spdlog::spdlog
235237
)
236238

239+
# --- UserTimestampedVideo example ---
240+
241+
add_executable(UserTimestampedVideoProducer
242+
user_timestamped_video/producer.cpp
243+
)
244+
245+
target_include_directories(UserTimestampedVideoProducer PRIVATE ${EXAMPLES_PRIVATE_INCLUDE_DIRS})
246+
247+
target_link_libraries(UserTimestampedVideoProducer
248+
PRIVATE
249+
livekit
250+
spdlog::spdlog
251+
)
252+
253+
add_executable(UserTimestampedVideoConsumer
254+
user_timestamped_video/consumer.cpp
255+
)
256+
257+
target_include_directories(UserTimestampedVideoConsumer PRIVATE ${EXAMPLES_PRIVATE_INCLUDE_DIRS})
258+
259+
target_link_libraries(UserTimestampedVideoConsumer
260+
PRIVATE
261+
livekit
262+
spdlog::spdlog
263+
)
264+
237265
add_custom_command(
238266
TARGET SimpleDataStream
239267
POST_BUILD

examples/common/sdl_media_manager.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -399,4 +399,4 @@ void SDLMediaManager::render() {
399399
if (renderer_running_.load(std::memory_order_relaxed) && sdl_renderer_) {
400400
sdl_renderer_->render();
401401
}
402-
}
402+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# UserTimestampedVideo
2+
3+
This example is split into two executables and can demonstrate all four
4+
producer/consumer combinations:
5+
6+
- `UserTimestampedVideoProducer` publishes a synthetic camera track and stamps
7+
each frame with `VideoCaptureOptions::metadata.user_timestamp_us`.
8+
- `UserTimestampedVideoConsumer` subscribes to remote camera frames with
9+
either the rich or legacy callback path.
10+
11+
Run them in the same room with different participant identities:
12+
13+
```sh
14+
LIVEKIT_URL=ws://localhost:7880 LIVEKIT_TOKEN=<producer-token> ./UserTimestampedVideoProducer
15+
LIVEKIT_URL=ws://localhost:7880 LIVEKIT_TOKEN=<consumer-token> ./UserTimestampedVideoConsumer
16+
```
17+
18+
Flags:
19+
20+
- Producer default: sends user timestamps
21+
- Producer `--without-user-timestamp`: does not send user timestamps
22+
- Consumer default: reads user timestamps through `setOnVideoFrameEventCallback`
23+
- Consumer `--ignore-user-timestamp`: ignores metadata through the legacy
24+
`setOnVideoFrameCallback`
25+
26+
Matrix:
27+
28+
```sh
29+
# 1. Producer sends, consumer reads
30+
./UserTimestampedVideoProducer
31+
./UserTimestampedVideoConsumer
32+
33+
# 2. Producer sends, consumer ignores
34+
./UserTimestampedVideoProducer
35+
./UserTimestampedVideoConsumer --ignore-user-timestamp
36+
37+
# 3. Producer does not send, consumer ignores
38+
./UserTimestampedVideoProducer --without-user-timestamp
39+
./UserTimestampedVideoConsumer --ignore-user-timestamp
40+
41+
# 4. Producer does not send, consumer reads
42+
./UserTimestampedVideoProducer --without-user-timestamp
43+
./UserTimestampedVideoConsumer
44+
```
45+
46+
Timestamp note:
47+
48+
- `user_ts_us` is application metadata and is the value to compare end to end.
49+
- `capture_ts_us` on the producer is the timestamp submitted to `captureFrame`.
50+
- `capture_ts_us` on the consumer is the received WebRTC frame timestamp.
51+
- Producer and consumer `capture_ts_us` values are not expected to match exactly,
52+
because WebRTC may translate frame timestamps onto its own internal
53+
capture-time timeline before delivery.
Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
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+
/*
18+
* UserTimestampedVideoConsumer
19+
*
20+
* Receives remote camera frames via Room::setOnVideoFrameEventCallback() and
21+
* logs any VideoFrameMetadata::user_timestamp_us values that arrive.
22+
*/
23+
24+
#include <atomic>
25+
#include <csignal>
26+
#include <cstdlib>
27+
#include <iostream>
28+
#include <mutex>
29+
#include <optional>
30+
#include <string>
31+
#include <thread>
32+
#include <unordered_set>
33+
#include <vector>
34+
35+
#include "livekit/livekit.h"
36+
37+
using namespace livekit;
38+
39+
namespace {
40+
41+
std::atomic<bool> g_running{true};
42+
43+
void handleSignal(int) { g_running.store(false); }
44+
45+
std::string getenvOrEmpty(const char *name) {
46+
const char *value = std::getenv(name);
47+
return value ? std::string(value) : std::string{};
48+
}
49+
50+
std::string
51+
formatUserTimestamp(const std::optional<VideoFrameMetadata> &metadata) {
52+
if (!metadata || !metadata->user_timestamp_us.has_value()) {
53+
return "n/a";
54+
}
55+
56+
return std::to_string(*metadata->user_timestamp_us);
57+
}
58+
59+
void printUsage(const char *program) {
60+
std::cerr << "Usage:\n"
61+
<< " " << program << " <ws-url> <token> [--ignore-user-timestamp]\n"
62+
<< "or:\n"
63+
<< " LIVEKIT_URL=... LIVEKIT_TOKEN=... " << program
64+
<< " [--ignore-user-timestamp]\n";
65+
}
66+
67+
bool parseArgs(int argc, char *argv[], std::string &url, std::string &token,
68+
bool &read_user_timestamp) {
69+
read_user_timestamp = true;
70+
std::vector<std::string> positional;
71+
72+
for (int i = 1; i < argc; ++i) {
73+
const std::string arg = argv[i];
74+
if (arg == "-h" || arg == "--help") {
75+
return false;
76+
}
77+
if (arg == "--ignore-user-timestamp") {
78+
read_user_timestamp = false;
79+
continue;
80+
}
81+
if (arg == "--read-user-timestamp") {
82+
read_user_timestamp = true;
83+
continue;
84+
}
85+
86+
positional.push_back(arg);
87+
}
88+
89+
url = getenvOrEmpty("LIVEKIT_URL");
90+
token = getenvOrEmpty("LIVEKIT_TOKEN");
91+
92+
if (positional.size() >= 2) {
93+
url = positional[0];
94+
token = positional[1];
95+
}
96+
97+
return !(url.empty() || token.empty());
98+
}
99+
100+
class UserTimestampedVideoConsumerDelegate : public RoomDelegate {
101+
public:
102+
UserTimestampedVideoConsumerDelegate(Room &room, bool read_user_timestamp)
103+
: room_(room), read_user_timestamp_(read_user_timestamp) {}
104+
105+
void registerExistingParticipants() {
106+
for (const auto &participant : room_.remoteParticipants()) {
107+
if (participant) {
108+
registerRemoteCameraCallback(participant->identity());
109+
}
110+
}
111+
}
112+
113+
void onParticipantConnected(Room &,
114+
const ParticipantConnectedEvent &event) override {
115+
if (!event.participant) {
116+
return;
117+
}
118+
119+
std::cout << "[consumer] participant connected: "
120+
<< event.participant->identity() << "\n";
121+
registerRemoteCameraCallback(event.participant->identity());
122+
}
123+
124+
void onParticipantDisconnected(
125+
Room &, const ParticipantDisconnectedEvent &event) override {
126+
if (!event.participant) {
127+
return;
128+
}
129+
130+
const std::string identity = event.participant->identity();
131+
room_.clearOnVideoFrameCallback(identity, TrackSource::SOURCE_CAMERA);
132+
133+
{
134+
std::lock_guard<std::mutex> lock(mutex_);
135+
registered_identities_.erase(identity);
136+
}
137+
138+
std::cout << "[consumer] participant disconnected: " << identity << "\n";
139+
}
140+
141+
private:
142+
void registerRemoteCameraCallback(const std::string &identity) {
143+
{
144+
std::lock_guard<std::mutex> lock(mutex_);
145+
if (!registered_identities_.insert(identity).second) {
146+
return;
147+
}
148+
}
149+
150+
VideoStream::Options stream_options;
151+
stream_options.format = VideoBufferType::RGBA;
152+
153+
if (read_user_timestamp_) {
154+
room_.setOnVideoFrameEventCallback(
155+
identity, TrackSource::SOURCE_CAMERA,
156+
[identity](const VideoFrameEvent &event) {
157+
std::cout << "[consumer] from=" << identity
158+
<< " size=" << event.frame.width() << "x"
159+
<< event.frame.height()
160+
<< " capture_ts_us=" << event.timestamp_us
161+
<< " user_ts_us=" << formatUserTimestamp(event.metadata)
162+
<< " rotation=" << static_cast<int>(event.rotation)
163+
<< "\n";
164+
},
165+
stream_options);
166+
} else {
167+
room_.setOnVideoFrameCallback(
168+
identity, TrackSource::SOURCE_CAMERA,
169+
[identity](const VideoFrame &frame, const std::int64_t timestamp_us) {
170+
std::cout << "[consumer] from=" << identity
171+
<< " size=" << frame.width() << "x" << frame.height()
172+
<< " capture_ts_us=" << timestamp_us
173+
<< " user_ts_us=ignored\n";
174+
},
175+
stream_options);
176+
}
177+
178+
std::cout << "[consumer] listening for camera frames from " << identity
179+
<< " with user timestamp "
180+
<< (read_user_timestamp_ ? "enabled" : "ignored") << "\n";
181+
}
182+
183+
Room &room_;
184+
bool read_user_timestamp_;
185+
std::mutex mutex_;
186+
std::unordered_set<std::string> registered_identities_;
187+
};
188+
189+
} // namespace
190+
191+
int main(int argc, char *argv[]) {
192+
std::string url;
193+
std::string token;
194+
bool read_user_timestamp = true;
195+
196+
if (!parseArgs(argc, argv, url, token, read_user_timestamp)) {
197+
printUsage(argv[0]);
198+
return 1;
199+
}
200+
201+
std::signal(SIGINT, handleSignal);
202+
#ifdef SIGTERM
203+
std::signal(SIGTERM, handleSignal);
204+
#endif
205+
206+
livekit::initialize(livekit::LogLevel::Info, livekit::LogSink::kConsole);
207+
int exit_code = 0;
208+
209+
{
210+
Room room;
211+
RoomOptions options;
212+
options.auto_subscribe = true;
213+
options.dynacast = false;
214+
215+
UserTimestampedVideoConsumerDelegate delegate(room, read_user_timestamp);
216+
room.setDelegate(&delegate);
217+
218+
std::cout << "[consumer] connecting to " << url << "\n";
219+
if (!room.Connect(url, token, options)) {
220+
std::cerr << "[consumer] failed to connect\n";
221+
exit_code = 1;
222+
} else {
223+
std::cout << "[consumer] connected as "
224+
<< room.localParticipant()->identity() << " to room '"
225+
<< room.room_info().name << "' with user timestamp "
226+
<< (read_user_timestamp ? "enabled" : "ignored") << "\n";
227+
228+
delegate.registerExistingParticipants();
229+
230+
while (g_running.load(std::memory_order_relaxed)) {
231+
std::this_thread::sleep_for(std::chrono::milliseconds(50));
232+
}
233+
234+
for (const auto &participant : room.remoteParticipants()) {
235+
if (participant) {
236+
room.clearOnVideoFrameCallback(participant->identity(),
237+
TrackSource::SOURCE_CAMERA);
238+
}
239+
}
240+
}
241+
242+
room.setDelegate(nullptr);
243+
}
244+
245+
livekit::shutdown();
246+
return exit_code;
247+
}

0 commit comments

Comments
 (0)