Skip to content

Commit a04dc57

Browse files
SubscriptionThreadDispatcher: guard against duplicate room events, duplicate room events test
1 parent 3c57c66 commit a04dc57

6 files changed

Lines changed: 803 additions & 45 deletions

File tree

include/livekit/remote_data_track.h

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -92,11 +92,6 @@ class RemoteDataTrack {
9292
/// @param options Pipeline options to apply to this remote data track.
9393
LIVEKIT_API void setPipelineOptions(const DataTrackPipelineOptions& options);
9494

95-
#ifdef LIVEKIT_TEST_ACCESS
96-
/// Test-only accessor for exercising lower-level FFI subscription paths.
97-
uintptr_t testFfiHandleId() const noexcept { return ffiHandleId(); }
98-
#endif
99-
10095
/// Subscribe to this remote data track.
10196
///
10297
/// Returns a DataTrackStream that delivers frames via blocking
@@ -106,8 +101,11 @@ class RemoteDataTrack {
106101

107102
private:
108103
friend class Room;
104+
#ifdef LIVEKIT_TEST_ACCESS
105+
friend struct RemoteDataTrackTestAccess;
106+
#endif
109107

110-
explicit RemoteDataTrack(const proto::OwnedRemoteDataTrack& owned);
108+
LIVEKIT_INTERNAL_API explicit RemoteDataTrack(const proto::OwnedRemoteDataTrack& owned);
111109

112110
uintptr_t ffiHandleId() const noexcept { return handle_.get(); }
113111
/// RAII wrapper for the Rust-owned FFI resource.

include/livekit/subscription_thread_dispatcher.h

Lines changed: 41 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616

1717
#pragma once
1818

19+
#include <atomic>
20+
#include <cstddef>
1921
#include <cstdint>
2022
#include <functional>
2123
#include <memory>
@@ -65,13 +67,15 @@ using DataFrameCallbackId = std::uint64_t;
6567
///
6668
/// `SubscriptionThreadDispatcher` is the low-level companion to @ref Room's
6769
/// remote track subscription flow. `Room` forwards user-facing callback
68-
/// registration requests here, and then calls @ref handleTrackSubscribed and
69-
/// @ref handleTrackUnsubscribed as room events arrive.
70+
/// registration requests here. For remote audio and video subscriptions it
71+
/// calls @ref handleTrackSubscribed and @ref handleTrackUnsubscribed; for
72+
/// data tracks it calls @ref handleDataTrackPublished and
73+
/// @ref handleDataTrackUnpublished.
7074
///
71-
/// For each registered `(participant identity, track name)` pair, this class
72-
/// may create a dedicated @ref AudioStream or @ref VideoStream and a matching
73-
/// reader thread. That thread blocks on stream reads and invokes the
74-
/// registered callback with decoded frames.
75+
/// For each registered audio or video `(participant identity, track name)`
76+
/// pair, this class may create a dedicated @ref AudioStream or @ref
77+
/// VideoStream and a matching reader thread. That thread blocks on stream
78+
/// reads and invokes the registered callback with decoded frames.
7579
///
7680
/// This type is intentionally independent from @ref RoomDelegate. High-level
7781
/// room events such as `RoomDelegate::onTrackSubscribed()` remain in @ref Room,
@@ -151,27 +155,33 @@ class LIVEKIT_API SubscriptionThreadDispatcher {
151155
/// @param track_name Track name to clear.
152156
void clearOnVideoFrameCallback(const std::string& participant_identity, const std::string& track_name);
153157

154-
/// Start or restart reader dispatch for a newly subscribed remote track.
158+
/// Start or restart reader dispatch for a newly subscribed remote audio or
159+
/// video track.
155160
///
156161
/// @ref Room calls this after it has processed a track-subscription event and
157-
/// updated its publication state. If a matching callback registration exists,
158-
/// the dispatcher creates the appropriate stream type and launches a reader
159-
/// thread for the `(participant, track_name)` key.
162+
/// updated its publication state. If a matching audio or video callback
163+
/// registration exists, the dispatcher creates the appropriate @ref
164+
/// AudioStream or @ref VideoStream and launches a reader thread for the
165+
/// `(participant, track_name)` key.
160166
///
161-
/// If no matching callback is registered, this is a no-op.
167+
/// Remote data tracks are handled separately via @ref handleDataTrackPublished.
168+
/// If @p track is not audio or video, or no matching callback is registered,
169+
/// this is a no-op.
162170
///
163171
/// @param participant_identity Identity of the remote participant.
164172
/// @param track_name Track name associated with the subscription.
165-
/// @param track Subscribed remote track to read from.
173+
/// @param track Subscribed remote audio or video track to read from.
166174
void handleTrackSubscribed(const std::string& participant_identity, const std::string& track_name,
167175
const std::shared_ptr<Track>& track);
168176

169-
/// Stop reader dispatch for an unsubscribed remote track.
177+
/// Stop reader dispatch for an unsubscribed remote audio or video track.
170178
///
171-
/// @ref Room calls this when a remote track is unsubscribed. Any active
172-
/// reader stream for the given `(participant, track_name)` key is closed and its
173-
/// thread is joined. Callback registration is preserved so future
174-
/// re-subscription can start dispatch again automatically.
179+
/// @ref Room calls this when a remote audio or video track is unsubscribed.
180+
/// Any active reader stream for the given `(participant, track_name)` key is
181+
/// closed and its thread is joined. Callback registration is preserved so
182+
/// future re-subscription can start dispatch again automatically.
183+
///
184+
/// Remote data tracks are handled separately via @ref handleDataTrackUnpublished.
175185
///
176186
/// @param participant_identity Identity of the remote participant.
177187
/// @param source Track source associated with the subscription.
@@ -259,6 +269,9 @@ class LIVEKIT_API SubscriptionThreadDispatcher {
259269
std::shared_ptr<AudioStream> audio_stream;
260270
std::shared_ptr<VideoStream> video_stream;
261271
std::thread thread;
272+
/// SID of the subscribed track backing this reader, used to skip redundant
273+
/// reader restarts when the same publication is re-subscribed.
274+
std::string track_sid;
262275
};
263276

264277
/// Compound lookup key for a remote participant identity and data track name.
@@ -289,6 +302,9 @@ class LIVEKIT_API SubscriptionThreadDispatcher {
289302
/// Active read-side resources for one data track stream subscription.
290303
struct ActiveDataReader {
291304
std::shared_ptr<RemoteDataTrack> remote_track;
305+
/// Set true when this reader is being replaced or torn down so the reader
306+
/// thread can abort a subscription that is still in flight.
307+
std::atomic<bool> cancelled{false};
292308
std::mutex sub_mutex;
293309
std::shared_ptr<DataTrackStream> stream; // guarded by sub_mutex
294310
std::thread thread;
@@ -333,18 +349,21 @@ class LIVEKIT_API SubscriptionThreadDispatcher {
333349
const RegisteredVideoCallback& callback);
334350

335351
/// Extract and close the data reader for a given callback ID, returning its
336-
/// thread. Must be called with @ref lock_ held.
352+
/// thread. Marks the reader cancelled so a subscription still in flight is
353+
/// aborted. Must be called with @ref lock_ held.
337354
std::thread extractDataReaderThreadLocked(DataFrameCallbackId id);
338355

339-
/// Extract and close the data reader for a given (participant, track_name)
340-
/// key, returning its thread. Must be called with @ref lock_ held.
341-
std::thread extractDataReaderThreadLocked(const DataCallbackKey& key);
342-
343356
/// Start a data reader thread for the given callback ID, key, and track.
344357
/// Must be called with @ref lock_ held.
345358
std::thread startDataReaderLocked(DataFrameCallbackId id, const DataCallbackKey& key,
346359
const std::shared_ptr<RemoteDataTrack>& track, const DataFrameCallback& cb);
347360

361+
/// Remove @p reader from @ref active_data_readers_ if the slot for @p id
362+
/// still refers to it. Called by the reader thread itself when it exits
363+
/// after a failed or cancelled subscription so it does not leave a stale
364+
/// entry behind. Acquires @ref lock_.
365+
void eraseDataReaderIfCurrent(DataFrameCallbackId id, const std::shared_ptr<ActiveDataReader>& reader);
366+
348367
/// Protects callback registration maps and active reader state.
349368
mutable std::mutex lock_;
350369

src/subscription_thread_dispatcher.cpp

Lines changed: 64 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,8 @@ void SubscriptionThreadDispatcher::handleDataTrackUnpublished(const std::string&
259259
for (auto it = active_data_readers_.begin(); it != active_data_readers_.end();) {
260260
auto& reader = it->second;
261261
if (reader->remote_track && reader->remote_track->info().sid == sid) {
262+
// Mark cancelled before closing to guard in flight subscriptions
263+
reader->cancelled = true;
262264
{
263265
const std::scoped_lock<std::mutex> sub_guard(reader->sub_mutex);
264266
if (reader->stream) {
@@ -312,6 +314,8 @@ void SubscriptionThreadDispatcher::stopAll() {
312314
video_callbacks_.clear();
313315

314316
for (auto& [id, reader] : active_data_readers_) {
317+
// Mark cancelled before closing to guard in flight subscriptions
318+
reader->cancelled = true;
315319
{
316320
const std::scoped_lock<std::mutex> sub_guard(reader->sub_mutex);
317321
if (reader->stream) {
@@ -397,6 +401,16 @@ std::thread SubscriptionThreadDispatcher::startAudioReaderLocked(const CallbackK
397401
const AudioFrameCallback& cb,
398402
const AudioStream::Options& opts) {
399403
LK_LOG_DEBUG("Starting audio reader for participant={} track_name={}", key.participant_identity, key.track_name);
404+
405+
auto existing = active_readers_.find(key);
406+
if (existing != active_readers_.end() && existing->second.track_sid == track->sid()) {
407+
LK_LOG_DEBUG(
408+
"Skipping audio reader start for participant={} track_name={} because a "
409+
"reader for sid={} is already active",
410+
key.participant_identity, key.track_name, track->sid());
411+
return {};
412+
}
413+
400414
auto old_thread = extractReaderThreadLocked(key);
401415

402416
if (static_cast<int>(active_readers_.size()) >= kMaxActiveReaders) {
@@ -415,6 +429,7 @@ std::thread SubscriptionThreadDispatcher::startAudioReaderLocked(const CallbackK
415429

416430
ActiveReader reader;
417431
reader.audio_stream = stream;
432+
reader.track_sid = track->sid();
418433
const std::string participant_identity = key.participant_identity;
419434
const std::string track_name = key.track_name;
420435
// NOLINTBEGIN(bugprone-lambda-function-name,bugprone-exception-escape)
@@ -454,6 +469,16 @@ std::thread SubscriptionThreadDispatcher::startVideoReaderLocked(const CallbackK
454469
const std::shared_ptr<Track>& track,
455470
const RegisteredVideoCallback& callback) {
456471
LK_LOG_DEBUG("Starting video reader for participant={} track_name={}", key.participant_identity, key.track_name);
472+
473+
auto existing = active_readers_.find(key);
474+
if (existing != active_readers_.end() && existing->second.track_sid == track->sid()) {
475+
LK_LOG_DEBUG(
476+
"Skipping video reader start for participant={} track_name={} because a "
477+
"reader for sid={} is already active",
478+
key.participant_identity, key.track_name, track->sid());
479+
return {};
480+
}
481+
457482
auto old_thread = extractReaderThreadLocked(key);
458483

459484
if (static_cast<int>(active_readers_.size()) >= kMaxActiveReaders) {
@@ -472,6 +497,7 @@ std::thread SubscriptionThreadDispatcher::startVideoReaderLocked(const CallbackK
472497

473498
ActiveReader reader;
474499
reader.video_stream = stream;
500+
reader.track_sid = track->sid();
475501
auto legacy_cb = callback.legacy_callback;
476502
auto event_cb = callback.event_callback;
477503
const std::string participant_identity = key.participant_identity;
@@ -522,6 +548,8 @@ std::thread SubscriptionThreadDispatcher::extractDataReaderThreadLocked(DataFram
522548
}
523549
auto reader = std::move(it->second);
524550
active_data_readers_.erase(it);
551+
// Mark cancelled before closing to guard in flight subscriptions
552+
reader->cancelled = true;
525553
{
526554
const std::scoped_lock<std::mutex> guard(reader->sub_mutex);
527555
if (reader->stream) {
@@ -531,28 +559,36 @@ std::thread SubscriptionThreadDispatcher::extractDataReaderThreadLocked(DataFram
531559
return std::move(reader->thread);
532560
}
533561

534-
std::thread SubscriptionThreadDispatcher::extractDataReaderThreadLocked(const DataCallbackKey& key) {
535-
for (auto it = active_data_readers_.begin(); it != active_data_readers_.end(); ++it) {
536-
if (it->second && it->second->remote_track &&
537-
it->second->remote_track->publisherIdentity() == key.participant_identity &&
538-
it->second->remote_track->info().name == key.track_name) {
539-
auto reader = std::move(it->second);
540-
active_data_readers_.erase(it);
541-
{
542-
const std::scoped_lock<std::mutex> guard(reader->sub_mutex);
543-
if (reader->stream) {
544-
reader->stream->close();
545-
}
546-
}
547-
return std::move(reader->thread);
548-
}
562+
void SubscriptionThreadDispatcher::eraseDataReaderIfCurrent(DataFrameCallbackId id,
563+
const std::shared_ptr<ActiveDataReader>& reader) {
564+
const std::scoped_lock<std::mutex> lock(lock_);
565+
auto it = active_data_readers_.find(id);
566+
if (it == active_data_readers_.end() || it->second != reader) {
567+
// The slot was already extracted or replaced; the owner joins that thread.
568+
return;
549569
}
550-
return {};
570+
// Detach so the reader can be destroyed by its own thread's final shared_ptr
571+
// release without tripping std::thread's joinable-at-destruction check. The
572+
// thread is already returning when this runs, so nothing is left to join.
573+
if (reader->thread.joinable()) {
574+
reader->thread.detach();
575+
}
576+
active_data_readers_.erase(it);
551577
}
552578

553579
std::thread SubscriptionThreadDispatcher::startDataReaderLocked(DataFrameCallbackId id, const DataCallbackKey& key,
554580
const std::shared_ptr<RemoteDataTrack>& track,
555581
const DataFrameCallback& cb) {
582+
auto existing = active_data_readers_.find(id);
583+
if (existing != active_data_readers_.end() && existing->second->remote_track &&
584+
existing->second->remote_track->info().sid == track->info().sid) {
585+
LK_LOG_DEBUG(
586+
"Skipping data reader start for \"{}\" track=\"{}\" because a reader for "
587+
"sid={} is already active",
588+
key.participant_identity, key.track_name, track->info().sid);
589+
return {};
590+
}
591+
556592
auto old_thread = extractDataReaderThreadLocked(id);
557593

558594
const int total_active = static_cast<int>(active_readers_.size()) + static_cast<int>(active_data_readers_.size());
@@ -571,7 +607,7 @@ std::thread SubscriptionThreadDispatcher::startDataReaderLocked(DataFrameCallbac
571607
auto identity = key.participant_identity;
572608
auto track_name = key.track_name;
573609
// NOLINTBEGIN(bugprone-lambda-function-name)
574-
reader->thread = std::thread([reader, track, cb, identity, track_name]() {
610+
reader->thread = std::thread([this, id, reader, track, cb, identity, track_name]() {
575611
LK_LOG_INFO("Data reader thread: subscribing to \"{}\" track=\"{}\"", identity, track_name);
576612
std::shared_ptr<DataTrackStream> stream;
577613
auto subscribe_result = track->subscribe();
@@ -581,13 +617,21 @@ std::thread SubscriptionThreadDispatcher::startDataReaderLocked(DataFrameCallbac
581617
"Failed to subscribe to data track \"{}\" from \"{}\": code={} "
582618
"message={}",
583619
track_name, identity, static_cast<std::uint32_t>(error.code), error.message);
620+
eraseDataReaderIfCurrent(id, reader);
584621
return;
585622
}
586623
stream = subscribe_result.value();
587624
LK_LOG_INFO("Data reader thread: subscribed to \"{}\" track=\"{}\"", identity, track_name);
588625

589626
{
590627
const std::scoped_lock<std::mutex> guard(reader->sub_mutex);
628+
// A replacement or teardown may have cancelled this reader while the
629+
// subscribe was in flight. Close the fresh stream and bail so we do not
630+
// leave a second live subscription behind.
631+
if (reader->cancelled.load()) {
632+
stream->close();
633+
return;
634+
}
591635
reader->stream = stream;
592636
}
593637

@@ -606,6 +650,9 @@ std::thread SubscriptionThreadDispatcher::startDataReaderLocked(DataFrameCallbac
606650
"\"{}\": code={} message={}",
607651
track_name, identity, static_cast<std::uint32_t>(error->code), error->message);
608652
}
653+
// Clean our own slot if the stream ended on its own (server EOS) and no
654+
// extract/teardown already claimed it. A no-op when we were extracted.
655+
eraseDataReaderIfCurrent(id, reader);
609656
LK_LOG_INFO("Data reader thread exiting for \"{}\" track=\"{}\"", identity, track_name);
610657
});
611658
// NOLINTEND(bugprone-lambda-function-name)

0 commit comments

Comments
 (0)