Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -93,3 +93,4 @@ add_subdirectory(simple_joystick_sender)
add_subdirectory(simple_joystick_receiver)
add_subdirectory(ping_pong_ping)
add_subdirectory(ping_pong_pong)
add_subdirectory(user_timestamped_video)
Comment thread
stephen-derosa marked this conversation as resolved.
16 changes: 16 additions & 0 deletions user_timestamped_video/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Copyright 2026 LiveKit, Inc.
#
# 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.

add_subdirectory(producer)
add_subdirectory(consumer)
66 changes: 66 additions & 0 deletions user_timestamped_video/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# UserTimestampedVideo

This example is split into two executables and can demonstrate all four
producer/consumer combinations:

- `UserTimestampedVideoProducer` publishes a synthetic video track named
`"timestamped-camera"` and stamps each frame with
`VideoCaptureOptions::metadata.user_timestamp_us`.
- `UserTimestampedVideoConsumer` subscribes to the remote
`"timestamped-camera"` track by name with either the rich or legacy callback
path.

Comment thread
stephen-derosa marked this conversation as resolved.
Run them in the same room with different participant identities:

```sh
LIVEKIT_URL=ws://localhost:7880 LIVEKIT_TOKEN=<producer-token> ./UserTimestampedVideoProducer
LIVEKIT_URL=ws://localhost:7880 LIVEKIT_TOKEN=<consumer-token> ./UserTimestampedVideoConsumer
```

Requirements:

- LiveKit C++ SDK `v0.3.4` or newer. This example uses
`VideoFrameMetadata` and `setOnVideoFrameEventCallback`, which are not
available in older SDK releases.
- To pin the SDK version when configuring the examples, pass
`-DLIVEKIT_SDK_VERSION=0.3.4` to CMake.

Flags:

- Producer default: sends user timestamps
- Producer `--with-user-timestamp`: explicitly sends user timestamps
- Producer `--without-user-timestamp`: does not send user timestamps
- Consumer default: reads user timestamps through `setOnVideoFrameEventCallback`
- Consumer `--with-user-timestamp`: explicitly reads user timestamps through
`setOnVideoFrameEventCallback`
- Consumer `--without-user-timestamp`: ignores metadata through the legacy
`setOnVideoFrameCallback`

Matrix:

```sh
# 1. Producer sends, consumer reads
./UserTimestampedVideoProducer
./UserTimestampedVideoConsumer

# 2. Producer sends, consumer ignores
./UserTimestampedVideoProducer
./UserTimestampedVideoConsumer --without-user-timestamp

# 3. Producer does not send, consumer ignores
./UserTimestampedVideoProducer --without-user-timestamp
./UserTimestampedVideoConsumer --without-user-timestamp

# 4. Producer does not send, consumer reads
./UserTimestampedVideoProducer --without-user-timestamp
./UserTimestampedVideoConsumer
```

Timestamp note:

- `user_ts_us` is application metadata and is the value to compare end to end.
- `capture_ts_us` on the producer is the timestamp submitted to `captureFrame`.
- `capture_ts_us` on the consumer is the received WebRTC frame timestamp.
- Producer and consumer `capture_ts_us` values are not expected to match exactly,
because WebRTC may translate frame timestamps onto its own internal
capture-time timeline before delivery.
105 changes: 105 additions & 0 deletions user_timestamped_video/common/cli_utils.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/*
* 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 <atomic>
#include <csignal>
#include <cstdlib>
#include <iostream>
#include <string>
#include <vector>

namespace user_timestamped_video {

enum class ParseResult { Ok, Help, Error };

struct CliOptions {
std::string url;
std::string token;
bool use_user_timestamp = true;
};

inline std::atomic<bool> g_running{true};

inline void handleSignal(int) { g_running.store(false); }

inline bool isRunning() { return g_running.load(std::memory_order_relaxed); }

inline void installSignalHandlers() {
std::signal(SIGINT, handleSignal);
#ifdef SIGTERM
std::signal(SIGTERM, handleSignal);
#endif
}

inline std::string getenvOrEmpty(const char *name) {
const char *value = std::getenv(name);
return value ? std::string(value) : std::string{};
}

inline void printUsage(const char *program) {
std::cerr << "Usage:\n"
<< " " << program << " <ws-url> <token> "
<< "[--with-user-timestamp|--without-user-timestamp]\n"
<< "or:\n"
<< " LIVEKIT_URL=... LIVEKIT_TOKEN=... " << program
<< " [--with-user-timestamp|--without-user-timestamp]\n";
}

inline ParseResult parseArgs(int argc, char *argv[], CliOptions &options) {
std::vector<std::string> positional;
options = CliOptions{};

for (int i = 1; i < argc; ++i) {
const std::string arg = argv[i];
if (arg == "-h" || arg == "--help") {
return ParseResult::Help;
}
if (arg == "--without-user-timestamp") {
options.use_user_timestamp = false;
continue;
}
if (arg == "--with-user-timestamp") {
options.use_user_timestamp = true;
continue;
}
if (!arg.empty() && arg[0] == '-') {
return ParseResult::Error;
}

positional.push_back(arg);
}

if (positional.size() > 2) {
return ParseResult::Error;
}

options.url = getenvOrEmpty("LIVEKIT_URL");
options.token = getenvOrEmpty("LIVEKIT_TOKEN");

if (positional.size() == 2) {
options.url = positional[0];
options.token = positional[1];
} else if (positional.size() == 1) {
return ParseResult::Error;
}

return (options.url.empty() || options.token.empty()) ? ParseResult::Error
: ParseResult::Ok;
}

} // namespace user_timestamped_video
22 changes: 22 additions & 0 deletions user_timestamped_video/consumer/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Copyright 2026 LiveKit, Inc.
#
# 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.

add_executable(UserTimestampedVideoConsumer
main.cpp
)

target_include_directories(UserTimestampedVideoConsumer PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(UserTimestampedVideoConsumer PRIVATE ${LIVEKIT_CORE_TARGET})

livekit_copy_windows_runtime_dlls(UserTimestampedVideoConsumer)
Loading
Loading