Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
d2c9858
add user timestamping feature
chenosaurus Feb 11, 2026
4d4027d
attach timestamp in example
chenosaurus Feb 11, 2026
b0d3c04
fix the latency calcs
chenosaurus Feb 12, 2026
90083c3
cleanup display overlays
chenosaurus Feb 12, 2026
f84fae3
rename flag
chenosaurus Feb 12, 2026
03083a0
add e2ee options
chenosaurus Feb 12, 2026
dd62177
display simulcast state
chenosaurus Feb 12, 2026
a742917
Merge branch 'main' into dc/feature/user_timestamp
chenosaurus Feb 12, 2026
d1b0d5c
use a mapping of rtp timestamp to user timestamp on subscriber side too
chenosaurus Feb 13, 2026
f68b83e
move the subscriber user timestamp handler to internal to clean up API
chenosaurus Feb 13, 2026
22f93d9
remove UserTimestamp store in favor of simple map to track ts
chenosaurus Feb 16, 2026
58eef75
Merge branch 'main' into dc/feature/user_timestamp
chenosaurus Feb 17, 2026
32b7278
use chrono crate instead of custom format func
chenosaurus Feb 17, 2026
63c68aa
remove comment
chenosaurus Feb 17, 2026
a60654f
change update to 2hz
chenosaurus Feb 17, 2026
867e295
remove last_user_timestamp
chenosaurus Feb 18, 2026
5a1ba5a
update readme
chenosaurus Feb 18, 2026
dde7de6
cargo fmt
chenosaurus Feb 18, 2026
eecec0e
remove unused callback reference
chenosaurus Feb 18, 2026
aa1cdb6
make the rtp lookup map insertion & removal more robust
chenosaurus Feb 18, 2026
2a52bbe
use new option for payload trailer features to enable timestamping
chenosaurus Mar 3, 2026
38734f4
remove timestamp logs
chenosaurus Mar 3, 2026
eea46e1
remove noisy logs
chenosaurus Mar 4, 2026
1c779a2
lint
chenosaurus Mar 4, 2026
6f172c1
update TrackPublishOptions in livekit-ffi
chenosaurus Mar 4, 2026
de9cd2b
adding the PayloadTrailerFeatures to the ffi protos
chenosaurus Mar 4, 2026
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
5 changes: 3 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion examples/local_video/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "local_video"
version = "0.1.0"
version = "0.2.0"
edition.workspace = true
publish = false

Expand Down Expand Up @@ -41,6 +41,7 @@ wgpu = "25.0"
winit = { version = "0.30.11", features = ["android-native-activity"] }
parking_lot = { workspace = true, features = ["deadlock_detection"] }
anyhow = { workspace = true }
chrono = "0.4"
bytemuck = { version = "1.16", features = ["derive"] }

nokhwa = { version = "0.10", default-features = false, features = ["output-threaded"] }
Expand Down
46 changes: 40 additions & 6 deletions examples/local_video/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,20 @@ Publisher usage:
--url https://your.livekit.server \
--api-key YOUR_KEY \
--api-secret YOUR_SECRET

# publish with a user timestamp attached to every frame
cargo run -p local_video -F desktop --bin publisher -- \
--camera-index 0 \
--room-name demo \
--identity cam-1 \
--attach-timestamp

# publish with end-to-end encryption
cargo run -p local_video -F desktop --bin publisher -- \
--camera-index 0 \
--room-name demo \
--identity cam-1 \
--e2ee-key my-secret-key
```

List devices usage:
Expand All @@ -41,6 +55,8 @@ Publisher flags (in addition to the common connection flags above):
- `--h265`: Use H.265/HEVC encoding if supported (falls back to H.264 on failure).
- `--simulcast`: Publish simulcast video (multiple layers when the resolution is large enough).
- `--max-bitrate <bps>`: Max video bitrate for the main (highest) layer in bits per second (e.g. `1500000`).
- `--attach-timestamp`: Attach the current wall-clock time (microseconds since UNIX epoch) as the user timestamp on each published frame. The subscriber can display this to measure end-to-end latency.
- `--e2ee-key <key>`: Enable end-to-end encryption with the given shared key. The subscriber must use the same key to decrypt.

Subscriber usage:
```
Expand All @@ -55,13 +71,31 @@ Subscriber usage:
--api-key YOUR_KEY \
--api-secret YOUR_SECRET

# subscribe to a specific participant's video only
cargo run -p local_video -F desktop --bin subscriber -- \
--room-name demo \
--identity viewer-1 \
--participant alice
# subscribe to a specific participant's video only
cargo run -p local_video -F desktop --bin subscriber -- \
--room-name demo \
--identity viewer-1 \
--participant alice

# display timestamp overlay (requires publisher to use --attach-timestamp)
cargo run -p local_video -F desktop --bin subscriber -- \
--room-name demo \
--identity viewer-1 \
--display-timestamp

# subscribe with end-to-end encryption (must match publisher's key)
cargo run -p local_video -F desktop --bin subscriber -- \
--room-name demo \
--identity viewer-1 \
--e2ee-key my-secret-key
```

Subscriber flags (in addition to the common connection flags above):
- `--participant <identity>`: Only subscribe to video tracks from the specified participant.
- `--display-timestamp`: Show a top-left overlay with the publisher's timestamp, the subscriber's current time, and the computed end-to-end latency. Requires the publisher to use `--attach-timestamp`.
- `--e2ee-key <key>`: Enable end-to-end decryption with the given shared key. Must match the key used by the publisher.

Notes:
- `--participant` limits subscription to video tracks from the specified participant identity.
- If the active video track is unsubscribed or unpublished, the app clears its state and will automatically attach to the next matching video track when it appears.
- For E2EE to work, both publisher and subscriber must specify the same `--e2ee-key` value. If the keys don't match, the subscriber will not be able to decode the video.
- The timestamp overlay updates at ~2 Hz so the latency value is readable rather than flickering every frame.
37 changes: 36 additions & 1 deletion examples/local_video/src/publisher.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use anyhow::Result;
use clap::Parser;
use livekit::e2ee::{key_provider::*, E2eeOptions, EncryptionType};
use livekit::options::{TrackPublishOptions, VideoCodec, VideoEncoding};
use livekit::prelude::*;
use livekit::webrtc::video_frame::{I420Buffer, VideoFrame, VideoRotation};
Expand All @@ -18,7 +19,7 @@ use std::sync::{
atomic::{AtomicBool, Ordering},
Arc,
};
use std::time::{Duration, Instant};
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
use yuv_sys;

#[derive(Parser, Debug)]
Expand Down Expand Up @@ -75,6 +76,14 @@ struct Args {
/// Use H.265/HEVC encoding if supported (falls back to H.264 on failure)
#[arg(long, default_value_t = false)]
h265: bool,

/// Attach the current system time (microseconds since UNIX epoch) as the user timestamp on each frame
#[arg(long, default_value_t = false)]
attach_timestamp: bool,

/// Shared encryption key for E2EE (enables AES-GCM end-to-end encryption when set)
#[arg(long)]
e2ee_key: Option<String>,
}

fn list_cameras() -> Result<()> {
Expand Down Expand Up @@ -137,10 +146,28 @@ async fn run(args: Args, ctrl_c_received: Arc<AtomicBool>) -> Result<()> {
info!("Connecting to LiveKit room '{}' as '{}'...", args.room_name, args.identity);
let mut room_options = RoomOptions::default();
room_options.auto_subscribe = true;

// Configure E2EE if an encryption key is provided
if let Some(ref e2ee_key) = args.e2ee_key {
let key_provider = KeyProvider::with_shared_key(
KeyProviderOptions::default(),
e2ee_key.as_bytes().to_vec(),
);
room_options.encryption =
Some(E2eeOptions { encryption_type: EncryptionType::Gcm, key_provider });
info!("E2EE enabled with AES-GCM encryption");
}

let (room, _) = Room::connect(&url, &token, room_options).await?;
let room = std::sync::Arc::new(room);
info!("Connected: {} - {}", room.name(), room.sid().await);

// Enable E2EE after connection
if args.e2ee_key.is_some() {
room.e2ee_manager().set_enabled(true);
info!("End-to-end encryption activated");
}

// Log room events
{
let room_clone = room.clone();
Expand Down Expand Up @@ -198,6 +225,7 @@ async fn run(args: Args, ctrl_c_received: Arc<AtomicBool>) -> Result<()> {
source: TrackSource::Camera,
simulcast: args.simulcast,
video_codec: codec,
user_timestamp: args.attach_timestamp,
..Default::default()
};
if let Some(bitrate) = args.max_bitrate {
Expand Down Expand Up @@ -230,6 +258,7 @@ async fn run(args: Args, ctrl_c_received: Arc<AtomicBool>) -> Result<()> {
let mut frame = VideoFrame {
rotation: VideoRotation::VideoRotation0,
timestamp_us: 0,
user_timestamp_us: None,
buffer: I420Buffer::new(width, height),
};
let is_yuyv = fmt.format() == FrameFormat::YUYV;
Expand Down Expand Up @@ -393,6 +422,12 @@ async fn run(args: Args, ctrl_c_received: Arc<AtomicBool>) -> Result<()> {

// Update RTP timestamp (monotonic, microseconds since start)
frame.timestamp_us = start_ts.elapsed().as_micros() as i64;
// Optionally attach wall-clock time as user timestamp
frame.user_timestamp_us = if args.attach_timestamp {
Some(SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_micros() as i64)
} else {
None
};
rtc_source.capture_frame(&frame);
let t4 = Instant::now();

Expand Down
Loading
Loading