1818#include < chrono>
1919#include < condition_variable>
2020#include < map>
21+ #include < memory>
2122#include < mutex>
23+ #include < sstream>
2224#include < string>
2325#include < thread>
26+ #include < vector>
2427
2528#include " ../common/audio_utils.h"
2629#include " ../common/test_common.h"
@@ -42,6 +45,7 @@ struct RoomEventCounts {
4245 std::map<std::string, int > participant_disconnected;
4346 std::map<std::string, int > track_published;
4447 std::map<std::string, int > track_subscribed;
48+ std::map<std::string, int > track_unsubscribed;
4549 std::map<std::string, int > track_unpublished;
4650 int disconnected = 0 ;
4751};
@@ -51,6 +55,7 @@ struct RoomEventCountsSnapshot {
5155 std::map<std::string, int > participant_disconnected;
5256 std::map<std::string, int > track_published;
5357 std::map<std::string, int > track_subscribed;
58+ std::map<std::string, int > track_unsubscribed;
5459 std::map<std::string, int > track_unpublished;
5560 int disconnected = 0 ;
5661};
@@ -62,6 +67,7 @@ RoomEventCountsSnapshot snapshotCounts(RoomEventCounts& counts) {
6267 snapshot.participant_disconnected = counts.participant_disconnected ;
6368 snapshot.track_published = counts.track_published ;
6469 snapshot.track_subscribed = counts.track_subscribed ;
70+ snapshot.track_unsubscribed = counts.track_unsubscribed ;
6571 snapshot.track_unpublished = counts.track_unpublished ;
6672 snapshot.disconnected = counts.disconnected ;
6773 return snapshot;
@@ -101,6 +107,13 @@ class RoomEventCounterDelegate : public RoomDelegate {
101107 notify ([&]() { incrementMap (counts_.track_subscribed , event.publication ->name ()); });
102108 }
103109
110+ void onTrackUnsubscribed (Room&, const TrackUnsubscribedEvent& event) override {
111+ if (event.publication == nullptr ) {
112+ return ;
113+ }
114+ notify ([&]() { incrementMap (counts_.track_unsubscribed , event.publication ->name ()); });
115+ }
116+
104117 void onTrackUnpublished (Room&, const TrackUnpublishedEvent& event) override {
105118 if (event.publication == nullptr ) {
106119 return ;
@@ -178,13 +191,97 @@ void expectCountsUnchangedAfterGrace(RoomEventCounts& counts, const RoomEventCou
178191 (std::string (phase) + " track_published duplicate" ).c_str ());
179192 expectMapCountsExact (after.track_subscribed , before.track_subscribed ,
180193 (std::string (phase) + " track_subscribed duplicate" ).c_str ());
194+ expectMapCountsExact (after.track_unsubscribed , before.track_unsubscribed ,
195+ (std::string (phase) + " track_unsubscribed duplicate" ).c_str ());
181196 expectMapCountsExact (after.track_unpublished , before.track_unpublished ,
182197 (std::string (phase) + " track_unpublished duplicate" ).c_str ());
183198 EXPECT_EQ (after.disconnected , before.disconnected ) << phase << " onDisconnected duplicate" ;
184199}
185200
186201std::string makeUniqueTrackName (const std::string& prefix) { return prefix + " -" + std::to_string (getTimestampUs ()); }
187202
203+ std::string describeCounts (const std::map<std::string, int >& counts) {
204+ std::ostringstream out;
205+ bool first = true ;
206+ out << " {" ;
207+ for (const auto & [key, count] : counts) {
208+ if (!first) {
209+ out << " , " ;
210+ }
211+ first = false ;
212+ out << key << " : " << count;
213+ }
214+ out << " }" ;
215+ return out.str ();
216+ }
217+
218+ class MediaLoopGuard {
219+ public:
220+ MediaLoopGuard () = default ;
221+ MediaLoopGuard (const MediaLoopGuard&) = delete ;
222+ MediaLoopGuard& operator =(const MediaLoopGuard&) = delete ;
223+
224+ ~MediaLoopGuard () { stop (); }
225+
226+ void addAudioSource (const std::shared_ptr<AudioSource>& source) {
227+ threads_.emplace_back ([this , source]() {
228+ runToneLoop (source, running_, 440.0 , false , kDefaultAudioSampleRate , kDefaultAudioChannels );
229+ });
230+ }
231+
232+ void addVideoSource (const std::shared_ptr<VideoSource>& source) {
233+ threads_.emplace_back ([this , source]() { runVideoLoop (source, running_, fillWebcamWrapper); });
234+ }
235+
236+ void stop () {
237+ running_.store (false , std::memory_order_relaxed);
238+ for (auto & thread : threads_) {
239+ if (thread.joinable ()) {
240+ thread.join ();
241+ }
242+ }
243+ }
244+
245+ private:
246+ std::atomic<bool > running_{true };
247+ std::vector<std::thread> threads_;
248+ };
249+
250+ class PublishedTrackGuard {
251+ public:
252+ explicit PublishedTrackGuard (LocalParticipant* participant) : participant_(participant) {}
253+ PublishedTrackGuard (const PublishedTrackGuard&) = delete ;
254+ PublishedTrackGuard& operator =(const PublishedTrackGuard&) = delete ;
255+
256+ ~PublishedTrackGuard () noexcept {
257+ try {
258+ unpublishAll ();
259+ } catch (...) {
260+ }
261+ }
262+
263+ void addTrackSid (const std::string& sid) {
264+ if (!sid.empty ()) {
265+ track_sids_.push_back (sid);
266+ }
267+ }
268+
269+ void unpublishAll () {
270+ if (participant_ != nullptr ) {
271+ for (const auto & sid : track_sids_) {
272+ if (!sid.empty ()) {
273+ participant_->unpublishTrack (sid);
274+ }
275+ }
276+ }
277+ track_sids_.clear ();
278+ }
279+
280+ private:
281+ LocalParticipant* participant_ = nullptr ;
282+ std::vector<std::string> track_sids_;
283+ };
284+
188285} // namespace
189286
190287class RoomEventDeduplicationIntegrationTest : public LiveKitTestBase , public ::testing::WithParamInterface<bool > {
@@ -222,6 +319,7 @@ TEST_P(RoomEventDeduplicationIntegrationTest, RoomLifecycleDelegateCallbacksFire
222319 const std::map<std::string, int > peer_identity_expected{{peer_identity, 1 }};
223320 ASSERT_TRUE (waitForMapCountAtLeast (observer_counts, peer_identity_expected, 1 , kEventWaitTimeout ))
224321 << " Timed out waiting for onParticipantConnected" ;
322+ ASSERT_TRUE (waitForParticipant (&observer_room, peer_identity, 10s)) << " Peer not visible to observer room" ;
225323 {
226324 const RoomEventCountsSnapshot snapshot = snapshotCounts (observer_counts);
227325 expectMapCountsExact (snapshot.participant_connected , peer_identity_expected, " onParticipantConnected" );
@@ -241,54 +339,46 @@ TEST_P(RoomEventDeduplicationIntegrationTest, RoomLifecycleDelegateCallbacksFire
241339 TrackPublishOptions video_opts;
242340 video_opts.source = TrackSource::SOURCE_CAMERA ;
243341
244- lockLocalParticipant (peer_room)->publishTrack (audio_track, audio_opts);
245- lockLocalParticipant (peer_room)->publishTrack (video_track, video_opts);
342+ auto peer_participant = lockLocalParticipant (peer_room);
343+ PublishedTrackGuard published_tracks (peer_participant.get ());
344+ MediaLoopGuard media_loops;
345+
346+ ASSERT_NO_THROW (peer_participant->publishTrack (audio_track, audio_opts));
246347 ASSERT_NE (audio_track->publication (), nullptr );
247- ASSERT_NE (video_track->publication (), nullptr );
348+ published_tracks.addTrackSid (audio_track->publication ()->sid ());
349+ media_loops.addAudioSource (audio_source);
248350
249- std::atomic<bool > media_running{true };
250- std::thread audio_thread ([&]() {
251- runToneLoop (audio_source, media_running, 440.0 , false , kDefaultAudioSampleRate , kDefaultAudioChannels );
252- });
253- std::thread video_thread ([&]() { runVideoLoop (video_source, media_running, fillWebcamWrapper); });
351+ ASSERT_NO_THROW (peer_participant->publishTrack (video_track, video_opts));
352+ ASSERT_NE (video_track->publication (), nullptr );
353+ published_tracks.addTrackSid (video_track->publication ()->sid ());
354+ media_loops.addVideoSource (video_source);
254355
255- const std::map<std::string, int > expected_published_counts{{audio_track_name, 1 }, {video_track_name, 1 }};
256356 const std::map<std::string, int > expected_subscribed_counts{{audio_track_name, 1 }, {video_track_name, 1 }};
257357
258- ASSERT_TRUE (waitForMapCountAtLeastTrack (observer_counts, expected_published_counts, &RoomEventCounts::track_published,
259- kEventWaitTimeout ))
260- << " Timed out waiting for onTrackPublished" ;
261358 ASSERT_TRUE (waitForMapCountAtLeastTrack (observer_counts, expected_subscribed_counts,
262359 &RoomEventCounts::track_subscribed, kEventWaitTimeout ))
263- << " Timed out waiting for onTrackSubscribed" ;
360+ << " Timed out waiting for onTrackSubscribed; observed track_subscribed="
361+ << describeCounts (snapshotCounts (observer_counts).track_subscribed );
264362
265363 {
266364 const RoomEventCountsSnapshot snapshot = snapshotCounts (observer_counts);
267- expectMapCountsExact (snapshot.track_published , expected_published_counts, " onTrackPublished" );
268365 expectMapCountsExact (snapshot.track_subscribed , expected_subscribed_counts, " onTrackSubscribed" );
269366 expectCountsUnchangedAfterGrace (observer_counts, snapshot, " after track subscribe" );
270367 }
271368
272- media_running.store (false , std::memory_order_relaxed);
273- if (audio_thread.joinable ()) {
274- audio_thread.join ();
275- }
276- if (video_thread.joinable ()) {
277- video_thread.join ();
278- }
279-
280- lockLocalParticipant (peer_room)->unpublishTrack (audio_track->publication ()->sid ());
281- lockLocalParticipant (peer_room)->unpublishTrack (video_track->publication ()->sid ());
369+ media_loops.stop ();
370+ published_tracks.unpublishAll ();
282371
283- const std::map<std::string, int > expected_unpublished_counts{{audio_track_name, 1 }, {video_track_name, 1 }};
284- ASSERT_TRUE (waitForMapCountAtLeastTrack (observer_counts, expected_unpublished_counts,
285- &RoomEventCounts::track_unpublished, kEventWaitTimeout ))
286- << " Timed out waiting for onTrackUnpublished" ;
372+ const std::map<std::string, int > expected_unsubscribed_counts{{audio_track_name, 1 }, {video_track_name, 1 }};
373+ ASSERT_TRUE (waitForMapCountAtLeastTrack (observer_counts, expected_unsubscribed_counts,
374+ &RoomEventCounts::track_unsubscribed, kEventWaitTimeout ))
375+ << " Timed out waiting for onTrackUnsubscribed; observed track_unsubscribed="
376+ << describeCounts (snapshotCounts (observer_counts).track_unsubscribed );
287377
288378 {
289379 const RoomEventCountsSnapshot snapshot = snapshotCounts (observer_counts);
290- expectMapCountsExact (snapshot.track_unpublished , expected_unpublished_counts , " onTrackUnpublished " );
291- expectCountsUnchangedAfterGrace (observer_counts, snapshot, " after track unpublished " );
380+ expectMapCountsExact (snapshot.track_unsubscribed , expected_unsubscribed_counts , " onTrackUnsubscribed " );
381+ expectCountsUnchangedAfterGrace (observer_counts, snapshot, " after track unsubscribed " );
292382 }
293383
294384 peer_room.disconnect ();
0 commit comments