Skip to content

Commit a53990a

Browse files
user_timestamped_video
1 parent f231c0c commit a53990a

7 files changed

Lines changed: 619 additions & 0 deletions

File tree

CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,3 +93,4 @@ add_subdirectory(simple_joystick_sender)
9393
add_subdirectory(simple_joystick_receiver)
9494
add_subdirectory(ping_pong_ping)
9595
add_subdirectory(ping_pong_pong)
96+
add_subdirectory(user_timestamped_video)
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Copyright 2026 LiveKit, Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
add_subdirectory(producer)
16+
add_subdirectory(consumer)

user_timestamped_video/README.md

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
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 video track named
7+
`"timestamped-camera"` and stamps each frame with
8+
`VideoCaptureOptions::metadata.user_timestamp_us`.
9+
- `UserTimestampedVideoConsumer` subscribes to the remote
10+
`"timestamped-camera"` track by name with either the rich or legacy callback
11+
path.
12+
13+
Run them in the same room with different participant identities:
14+
15+
```sh
16+
LIVEKIT_URL=ws://localhost:7880 LIVEKIT_TOKEN=<producer-token> ./UserTimestampedVideoProducer
17+
LIVEKIT_URL=ws://localhost:7880 LIVEKIT_TOKEN=<consumer-token> ./UserTimestampedVideoConsumer
18+
```
19+
20+
Requirements:
21+
22+
- LiveKit C++ SDK `v0.3.4` or newer. This example uses
23+
`VideoFrameMetadata` and `setOnVideoFrameEventCallback`, which are not
24+
available in older SDK releases.
25+
- To pin the SDK version when configuring the examples, pass
26+
`-DLIVEKIT_SDK_VERSION=0.3.4` to CMake.
27+
28+
Flags:
29+
30+
- Producer default: sends user timestamps
31+
- Producer `--with-user-timestamp`: explicitly sends user timestamps
32+
- Producer `--without-user-timestamp`: does not send user timestamps
33+
- Consumer default: reads user timestamps through `setOnVideoFrameEventCallback`
34+
- Consumer `--with-user-timestamp`: explicitly reads user timestamps through
35+
`setOnVideoFrameEventCallback`
36+
- Consumer `--without-user-timestamp`: ignores metadata through the legacy
37+
`setOnVideoFrameCallback`
38+
39+
Matrix:
40+
41+
```sh
42+
# 1. Producer sends, consumer reads
43+
./UserTimestampedVideoProducer
44+
./UserTimestampedVideoConsumer
45+
46+
# 2. Producer sends, consumer ignores
47+
./UserTimestampedVideoProducer
48+
./UserTimestampedVideoConsumer --without-user-timestamp
49+
50+
# 3. Producer does not send, consumer ignores
51+
./UserTimestampedVideoProducer --without-user-timestamp
52+
./UserTimestampedVideoConsumer --without-user-timestamp
53+
54+
# 4. Producer does not send, consumer reads
55+
./UserTimestampedVideoProducer --without-user-timestamp
56+
./UserTimestampedVideoConsumer
57+
```
58+
59+
Timestamp note:
60+
61+
- `user_ts_us` is application metadata and is the value to compare end to end.
62+
- `capture_ts_us` on the producer is the timestamp submitted to `captureFrame`.
63+
- `capture_ts_us` on the consumer is the received WebRTC frame timestamp.
64+
- Producer and consumer `capture_ts_us` values are not expected to match exactly,
65+
because WebRTC may translate frame timestamps onto its own internal
66+
capture-time timeline before delivery.
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Copyright 2026 LiveKit, Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
add_executable(UserTimestampedVideoConsumer
16+
main.cpp
17+
)
18+
19+
target_include_directories(UserTimestampedVideoConsumer PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
20+
target_link_libraries(UserTimestampedVideoConsumer PRIVATE ${LIVEKIT_CORE_TARGET})
21+
22+
livekit_copy_windows_runtime_dlls(UserTimestampedVideoConsumer)
Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
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+
/// UserTimestampedVideoConsumer
18+
///
19+
/// Receives remote video frames via `Room::setOnVideoFrameEventCallback()` and
20+
/// logs any `VideoFrameMetadata::user_timestamp_us` values that arrive. Pair
21+
/// with `UserTimestampedVideoProducer` running in another process.
22+
///
23+
/// Usage:
24+
/// UserTimestampedVideoConsumer <ws-url> <token>
25+
/// [--with-user-timestamp|--without-user-timestamp]
26+
///
27+
/// Or via environment variables:
28+
/// LIVEKIT_URL, LIVEKIT_TOKEN
29+
30+
#include <atomic>
31+
#include <chrono>
32+
#include <csignal>
33+
#include <cstdint>
34+
#include <cstdlib>
35+
#include <iostream>
36+
#include <mutex>
37+
#include <optional>
38+
#include <string>
39+
#include <thread>
40+
#include <unordered_set>
41+
#include <vector>
42+
43+
#include "livekit/livekit.h"
44+
45+
using namespace livekit;
46+
47+
namespace {
48+
49+
constexpr const char *kTrackName = "timestamped-camera";
50+
51+
std::atomic<bool> g_running{true};
52+
53+
enum class ParseResult { Ok, Help, Error };
54+
55+
void handleSignal(int) { g_running.store(false); }
56+
57+
std::string getenvOrEmpty(const char *name) {
58+
const char *value = std::getenv(name);
59+
return value ? std::string(value) : std::string{};
60+
}
61+
62+
std::string
63+
formatUserTimestamp(const std::optional<VideoFrameMetadata> &metadata) {
64+
if (!metadata || !metadata->user_timestamp_us.has_value()) {
65+
return "n/a";
66+
}
67+
68+
return std::to_string(*metadata->user_timestamp_us);
69+
}
70+
71+
void printUsage(const char *program) {
72+
std::cerr << "Usage:\n"
73+
<< " " << program << " <ws-url> <token> "
74+
<< "[--with-user-timestamp|--without-user-timestamp]\n"
75+
<< "or:\n"
76+
<< " LIVEKIT_URL=... LIVEKIT_TOKEN=... " << program
77+
<< " [--with-user-timestamp|--without-user-timestamp]\n";
78+
}
79+
80+
ParseResult parseArgs(int argc, char *argv[], std::string &url,
81+
std::string &token, bool &read_user_timestamp) {
82+
read_user_timestamp = true;
83+
std::vector<std::string> positional;
84+
85+
for (int i = 1; i < argc; ++i) {
86+
const std::string arg = argv[i];
87+
if (arg == "-h" || arg == "--help") {
88+
return ParseResult::Help;
89+
}
90+
if (arg == "--without-user-timestamp") {
91+
read_user_timestamp = false;
92+
continue;
93+
}
94+
if (arg == "--with-user-timestamp") {
95+
read_user_timestamp = true;
96+
continue;
97+
}
98+
99+
positional.push_back(arg);
100+
}
101+
102+
url = getenvOrEmpty("LIVEKIT_URL");
103+
token = getenvOrEmpty("LIVEKIT_TOKEN");
104+
105+
if (positional.size() >= 2) {
106+
url = positional[0];
107+
token = positional[1];
108+
}
109+
110+
return (url.empty() || token.empty()) ? ParseResult::Error : ParseResult::Ok;
111+
}
112+
113+
class UserTimestampedVideoConsumerDelegate : public RoomDelegate {
114+
public:
115+
UserTimestampedVideoConsumerDelegate(Room &room, bool read_user_timestamp)
116+
: room_(room), read_user_timestamp_(read_user_timestamp) {}
117+
118+
void registerExistingParticipants() {
119+
for (const auto &participant : room_.remoteParticipants()) {
120+
if (participant) {
121+
registerRemoteVideoCallback(participant->identity());
122+
}
123+
}
124+
}
125+
126+
void onParticipantConnected(Room &,
127+
const ParticipantConnectedEvent &event) override {
128+
if (!event.participant) {
129+
return;
130+
}
131+
132+
std::cout << "[consumer] participant connected: "
133+
<< event.participant->identity() << "\n";
134+
registerRemoteVideoCallback(event.participant->identity());
135+
}
136+
137+
void onParticipantDisconnected(
138+
Room &, const ParticipantDisconnectedEvent &event) override {
139+
if (!event.participant) {
140+
return;
141+
}
142+
143+
const std::string identity = event.participant->identity();
144+
room_.clearOnVideoFrameCallback(identity, std::string(kTrackName));
145+
146+
{
147+
std::lock_guard<std::mutex> lock(mutex_);
148+
registered_identities_.erase(identity);
149+
}
150+
151+
std::cout << "[consumer] participant disconnected: " << identity << "\n";
152+
}
153+
154+
private:
155+
void registerRemoteVideoCallback(const std::string &identity) {
156+
{
157+
std::lock_guard<std::mutex> lock(mutex_);
158+
if (!registered_identities_.insert(identity).second) {
159+
return;
160+
}
161+
}
162+
163+
VideoStream::Options stream_options;
164+
stream_options.format = VideoBufferType::RGBA;
165+
166+
if (read_user_timestamp_) {
167+
room_.setOnVideoFrameEventCallback(
168+
identity, std::string(kTrackName),
169+
[identity](const VideoFrameEvent &event) {
170+
std::cout << "[consumer] from=" << identity
171+
<< " size=" << event.frame.width() << "x"
172+
<< event.frame.height()
173+
<< " capture_ts_us=" << event.timestamp_us
174+
<< " user_ts_us=" << formatUserTimestamp(event.metadata)
175+
<< " rotation=" << static_cast<int>(event.rotation)
176+
<< "\n";
177+
},
178+
stream_options);
179+
} else {
180+
room_.setOnVideoFrameCallback(
181+
identity, std::string(kTrackName),
182+
[identity](const VideoFrame &frame, const std::int64_t timestamp_us) {
183+
std::cout << "[consumer] from=" << identity
184+
<< " size=" << frame.width() << "x" << frame.height()
185+
<< " capture_ts_us=" << timestamp_us
186+
<< " user_ts_us=ignored\n";
187+
},
188+
stream_options);
189+
}
190+
191+
std::cout << "[consumer] listening for video frames from " << identity
192+
<< " track=\"" << kTrackName << "\" with user timestamp "
193+
<< (read_user_timestamp_ ? "enabled" : "ignored") << "\n";
194+
}
195+
196+
Room &room_;
197+
bool read_user_timestamp_;
198+
std::mutex mutex_;
199+
std::unordered_set<std::string> registered_identities_;
200+
};
201+
202+
} // namespace
203+
204+
int main(int argc, char *argv[]) {
205+
std::string url;
206+
std::string token;
207+
bool read_user_timestamp = true;
208+
209+
const ParseResult parse_result =
210+
parseArgs(argc, argv, url, token, read_user_timestamp);
211+
if (parse_result != ParseResult::Ok) {
212+
printUsage(argv[0]);
213+
return parse_result == ParseResult::Help ? 0 : 1;
214+
}
215+
216+
std::signal(SIGINT, handleSignal);
217+
#ifdef SIGTERM
218+
std::signal(SIGTERM, handleSignal);
219+
#endif
220+
221+
livekit::initialize(livekit::LogLevel::Info, livekit::LogSink::kConsole);
222+
int exit_code = 0;
223+
224+
{
225+
Room room;
226+
RoomOptions options;
227+
options.auto_subscribe = true;
228+
options.dynacast = false;
229+
230+
UserTimestampedVideoConsumerDelegate delegate(room, read_user_timestamp);
231+
room.setDelegate(&delegate);
232+
233+
std::cout << "[consumer] connecting to " << url << "\n";
234+
if (!room.Connect(url, token, options)) {
235+
std::cerr << "[consumer] failed to connect\n";
236+
exit_code = 1;
237+
} else {
238+
std::cout << "[consumer] connected as "
239+
<< room.localParticipant()->identity() << " to room '"
240+
<< room.room_info().name << "' with user timestamp "
241+
<< (read_user_timestamp ? "enabled" : "ignored") << "\n";
242+
243+
delegate.registerExistingParticipants();
244+
245+
while (g_running.load(std::memory_order_relaxed)) {
246+
std::this_thread::sleep_for(std::chrono::milliseconds(50));
247+
}
248+
249+
for (const auto &participant : room.remoteParticipants()) {
250+
if (participant) {
251+
room.clearOnVideoFrameCallback(participant->identity(),
252+
std::string(kTrackName));
253+
}
254+
}
255+
}
256+
257+
room.setDelegate(nullptr);
258+
}
259+
260+
livekit::shutdown();
261+
return exit_code;
262+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Copyright 2026 LiveKit, Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
add_executable(UserTimestampedVideoProducer
16+
main.cpp
17+
)
18+
19+
target_include_directories(UserTimestampedVideoProducer PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
20+
target_link_libraries(UserTimestampedVideoProducer PRIVATE ${LIVEKIT_CORE_TARGET})
21+
22+
livekit_copy_windows_runtime_dlls(UserTimestampedVideoProducer)

0 commit comments

Comments
 (0)