Skip to content

Comments

Add support for frame level user timestamp#890

Open
chenosaurus wants to merge 20 commits intomainfrom
dc/feature/user_timestamp
Open

Add support for frame level user timestamp#890
chenosaurus wants to merge 20 commits intomainfrom
dc/feature/user_timestamp

Conversation

@chenosaurus
Copy link
Contributor

@chenosaurus chenosaurus commented Feb 12, 2026

  • Add support to attach/parse frame level timestamps to VideoTracks as a custom payload trailer.

@chenosaurus chenosaurus marked this pull request as ready for review February 18, 2026 01:08
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds comprehensive support for frame-level user timestamps in video tracks, enabling end-to-end timestamp propagation through the WebRTC pipeline. The feature embeds custom timestamps as 12-byte trailers (8 bytes timestamp + 4 bytes "LKTS" magic marker) on encoded frames, which are extracted on the receiver side and made available to applications for latency measurement and frame correlation.

Changes:

  • Implements UserTimestampTransformer for embedding/extracting timestamps in RTP frames
  • Integrates timestamp handling with E2EE frame cryptor through chained transformers
  • Adds API surface on video tracks, sources, and streams for timestamp management
  • Updates examples with E2EE support and timestamp visualization

Reviewed changes

Copilot reviewed 35 out of 36 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
webrtc-sys/include/livekit/user_timestamp.h Core transformer interface and handler class definitions
webrtc-sys/src/user_timestamp.cpp Implementation of timestamp embedding/extraction logic with LRU-based map eviction
webrtc-sys/src/user_timestamp.rs Rust FFI bindings for timestamp handler
webrtc-sys/src/video_track.{h,cpp,rs} Extended video track source to accept and forward user timestamps
webrtc-sys/src/frame_cryptor.{h,cpp,rs} Added chaining support to combine timestamp and encryption transformers
libwebrtc/src/native/user_timestamp.rs High-level Rust API for timestamp handlers
libwebrtc/src/native/video_{source,stream,track}.rs Integrated timestamp handlers into video pipeline
livekit/src/room/e2ee/manager.rs Automatic timestamp handler setup for all video tracks
livekit/src/room/track/{local,remote}_video_track.rs Public API for accessing timestamp handlers
examples/local_video/src/{publisher,subscriber}.rs Added E2EE support and timestamp visualization
examples/local_video/README.md Documentation for new timestamp and E2EE features

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 77 to 80
private:
rtc::scoped_refptr<webrtc::FrameTransformerInterface> first_;
rtc::scoped_refptr<webrtc::FrameTransformerInterface> second_;
rtc::scoped_refptr<webrtc::TransformedFrameCallback> callback_;
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ChainedFrameTransformer stores the callback_ but never uses it. When the first transformer completes and calls OnTransformedFrame, the frame is forwarded to the second transformer, which then calls its own registered callback directly. The stored callback_ field is redundant and could be removed to simplify the code.

Consider removing the callback_ member variable since it serves no purpose in the current implementation.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed the unused ref

Comment on lines 182 to 183
recv_map_[rtp_timestamp] = user_ts.value();
recv_map_order_.push_back(rtp_timestamp);
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The same duplicate entry issue exists for the receive map. If a frame with the same RTP timestamp is received multiple times (e.g., due to retransmission), the order queue will contain duplicates while the map has only one entry. This can lead to incorrect eviction behavior.

Consider checking if the RTP timestamp already exists before adding to the order queue, or handle potential duplicates in the eviction logic.

Copilot uses AI. Check for mistakes.
Comment on lines +121 to +129
RTC_LOG(LS_INFO) << "UserTimestampTransformer::TransformSend appended "
"trailer"
<< " ts_us=" << ts_to_embed
<< " rtp_ts=" << rtp_timestamp
<< " ssrc=" << ssrc
<< " capture_us="
<< (capture_time.has_value() ? capture_time->us() : -1)
<< " orig_size=" << data.size()
<< " new_size=" << new_data.size();
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using LS_INFO log level for per-frame operations will generate extremely verbose logs in production. Since this logs on every frame (potentially 30-60 times per second), it could significantly impact performance and log file sizes.

Consider using LS_VERBOSE or adding a runtime configuration flag to control this level of logging detail.

Copilot uses AI. Check for mistakes.
Comment on lines 375 to 376
send_map_[key] = user_timestamp_us;
send_map_order_.push_back(key);
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential issue: If the same key (truncated capture timestamp) is stored multiple times, it will be inserted into send_map_order_ repeatedly, but the map will only contain one entry. This means send_map_order_ can contain duplicate keys, causing incorrect eviction behavior where the wrong entries might be evicted or entries might remain in the order queue after being erased from the map.

Consider checking if the key already exists in the map before adding it to the order queue, or remove existing entries from the order queue before re-adding them.

Copilot uses AI. Check for mistakes.
Comment on lines +189 to +192
RTC_LOG(LS_INFO) << "UserTimestampTransformer"
<< " user_ts=" << user_ts.value()
<< " rtp_ts=" << frame->GetTimestamp()
<< " recv_latency=" << recv_latency_ms << " ms";
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using LS_INFO log level for per-frame operations will generate extremely verbose logs. Since this logs on every received frame, it could significantly impact performance and log file sizes in production.

Consider using LS_VERBOSE for per-frame logs and reserving LS_INFO for connection-level events.

Copilot uses AI. Check for mistakes.
Comment on lines +378 to +382
RTC_LOG(LS_INFO) << "UserTimestampTransformer::store_user_timestamp"
<< " capture_ts_us=" << capture_timestamp_us
<< " key_us=" << key
<< " user_ts_us=" << user_timestamp_us
<< " size=" << send_map_.size();
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using LS_INFO log level for every call to store_user_timestamp will generate extremely verbose logs. Since this is called for every captured frame (potentially 30-60 times per second), it could significantly impact performance.

Consider using LS_VERBOSE or only logging at LS_INFO when the map reaches certain thresholds.

Copilot uses AI. Check for mistakes.
Comment on lines +49 to +52
RTC_LOG(LS_INFO) << "UserTimestampTransformer::Transform (disabled)"
<< " direction="
<< (direction_ == Direction::kSend ? "send" : "recv")
<< " ssrc=" << ssrc << " rtp_ts=" << rtp_timestamp;
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using LS_INFO log level for disabled transformer pass-through will log on every frame even when the feature is disabled. This could generate significant log noise in production.

Consider using LS_VERBOSE for this diagnostic message or removing it entirely since pass-through is the expected behavior when disabled.

Copilot uses AI. Check for mistakes.
Comment on lines +89 to +95
log::info!(
target: "user_timestamp",
"store: capture_ts_us={}, user_ts_us={}",
capture_timestamp_us,
user_timestamp_us
);
self.sys_handle.store_user_timestamp(capture_timestamp_us, user_timestamp_us);
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The log::info! call on every store_user_timestamp invocation will generate very verbose logs (potentially 30-60 times per second for video frames). This appears to be debug-level logging rather than info-level.

Consider using log::debug! or log::trace! instead of log::info! for per-frame operations.

Copilot uses AI. Check for mistakes.
// Route user timestamp transformer logs to a dedicated target so they can
// be enabled independently from the very noisy general libwebrtc logs.
if msg.contains("UserTimestampTransformer") {
log::info!(target: "user_timestamp_rtp", "{}", msg);
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The log routing logic uses log::info! for user timestamp transformer logs, which means these very verbose per-frame logs will be at info level. Since the transformer logs on every single frame, this could still be quite noisy even with a dedicated log target.

Consider routing these to log::debug! or log::trace! instead of log::info! so they can be filtered out more easily in production.

Suggested change
log::info!(target: "user_timestamp_rtp", "{}", msg);
log::trace!(target: "user_timestamp_rtp", "{}", msg);

Copilot uses AI. Check for mistakes.
@chenosaurus chenosaurus requested a review from 1egoman February 18, 2026 20:38
@theomonnom
Copy link
Member

Is there a way to ensure that any client can still decode videos, even if they don’t implement user timestamps?

@chenosaurus
Copy link
Contributor Author

Is there a way to ensure that any client can still decode videos, even if they don’t implement user timestamps?

@theomonnom if an old SDK is used to receive a video track that has timestamps attached, it will not be able to decode. We had discussed this and the consensus was that this is something that a customer would need to ensure is enabled on all of their SDKs if they want to use it. W/ the trailer attached, the video does not decode.

If we wanted to be able to allow clients that doesn't have this feature to be able to decode video, we'd have to do something like check the sdk version and strip the trailer at the SFU before sending downstream to client.

@theomonnom
Copy link
Member

Is there a way to ensure that any client can still decode videos, even if they don’t implement user timestamps?

@theomonnom if an old SDK is used to receive a video track that has timestamps attached, it will not be able to decode. We had discussed this and the consensus was that this is something that a customer would need to ensure is enabled on all of their SDKs if they want to use it. W/ the trailer attached, the video does not decode.

If we wanted to be able to allow clients that doesn't have this feature to be able to decode video, we'd have to do something like check the sdk version and strip the trailer at the SFU before sending downstream to client.

Stripping the trailer is a cheap operation tho no?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants