Skip to content

Commit 087f046

Browse files
fix PC timeout when connecting with can_subscribe=false (#955)
1 parent c863be7 commit 087f046

3 files changed

Lines changed: 69 additions & 3 deletions

File tree

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
livekit-ffi: patch
3+
webrtc-sys-build: patch
4+
yuv-sys: patch
5+
imgproc: patch
6+
livekit-protocol: patch
7+
webrtc-sys: patch
8+
livekit: patch
9+
libwebrtc: patch
10+
livekit-wakeword: patch
11+
soxr-sys: patch
12+
livekit-api: patch
13+
---
14+
15+
# fix PC timeout when connecting with can_subscribe=false
16+
17+
#955 by @s-hamdananwar
18+
19+
When a participant connects with `canSubscribe=false` in their token, the server sends `subscriber_primary=false` in the JoinResponse and does not send a subscriber offer. This results in `wait_pc_connection` timing out as it is expecting a subscriber PC even when the publisher PC is primary. This PR will skip waiting for subscriber PC when `subscriber_primary=false`.

livekit/src/rtc_engine/rtc_session.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,7 @@ struct SessionInner {
376376
pending_requests: Mutex<HashMap<u32, oneshot::Sender<proto::RequestResponse>>>,
377377

378378
e2ee_manager: Option<E2eeManager>,
379+
subscriber_primary: bool,
379380
}
380381

381382
/// Information about the local participant needed for outgoing
@@ -431,6 +432,7 @@ impl RtcSession {
431432
SignalClient::connect(url, token, options.signal_options.clone()).await?;
432433
let signal_client = Arc::new(signal_client);
433434
log::debug!("received JoinResponse: {:?}", join_response);
435+
let subscriber_primary = join_response.subscriber_primary;
434436

435437
// Determine if single PC mode is active based on the path used
436438
let single_pc_mode = signal_client.is_single_pc_mode_active();
@@ -517,6 +519,7 @@ impl RtcSession {
517519
negotiation_queue: NegotiationQueue::new(),
518520
pending_requests: Default::default(),
519521
e2ee_manager,
522+
subscriber_primary,
520523
});
521524

522525
// Start session tasks
@@ -530,7 +533,7 @@ impl RtcSession {
530533

531534
// In single PC mode (or with fast_publish), trigger initial negotiation
532535
// This matches JS SDK behavior: if (!this.subscriberPrimary || joinResponse.fastPublish) { this.negotiate(); }
533-
if single_pc_mode || join_response.fast_publish {
536+
if single_pc_mode || join_response.fast_publish || !subscriber_primary {
534537
inner.publisher_negotiation_needed();
535538
}
536539

@@ -1750,8 +1753,8 @@ impl SessionInner {
17501753
}
17511754

17521755
let publisher_connected = self.publisher_pc.is_connected();
1753-
let subscriber_connected = if self.single_pc_mode {
1754-
true // No subscriber in single PC mode
1756+
let subscriber_connected = if self.single_pc_mode || !self.subscriber_primary {
1757+
true // No subscriber in single PC mode or if PC is publisher primary
17551758
} else {
17561759
self.subscriber_pc.as_ref().map(|pc| pc.is_connected()).unwrap_or(true)
17571760
};

livekit/tests/peer_connection_signaling_test.rs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,17 @@ async fn test_v1_localhost_fallback_to_v0() -> Result<()> {
298298
Ok(())
299299
}
300300

301+
/// Test that a participant with can_subscribe=false in their token can connect without timing out.
302+
#[test_log::test(tokio::test)]
303+
async fn test_v0_connect_can_subscribe_false() -> Result<()> {
304+
test_connect_can_subscribe_false_impl(SignalingMode::DualPC).await
305+
}
306+
307+
#[test_log::test(tokio::test)]
308+
async fn test_v1_connect_can_subscribe_false() -> Result<()> {
309+
test_connect_can_subscribe_false_impl(SignalingMode::SinglePC).await
310+
}
311+
301312
/// Corner case: reconnect twice in a row
302313
#[test_log::test(tokio::test)]
303314
async fn test_v0_double_reconnect() -> Result<()> {
@@ -772,6 +783,39 @@ async fn test_node_failure_impl(mode: SignalingMode) -> Result<()> {
772783
Ok(())
773784
}
774785

786+
/// Test that a participant with can_subscribe=false in their token can connect without timing out.
787+
async fn test_connect_can_subscribe_false_impl(mode: SignalingMode) -> Result<()> {
788+
let (url, api_key, api_secret) = get_env_for_mode(mode);
789+
let room_name = format!("test_{:?}_no_subscribe_{}", mode, create_random_uuid());
790+
791+
let grants = VideoGrants {
792+
room_join: true,
793+
room: room_name.clone(),
794+
can_publish: true,
795+
can_subscribe: false,
796+
..Default::default()
797+
};
798+
let token = AccessToken::with_api_key(&api_key, &api_secret)
799+
.with_ttl(Duration::from_secs(30 * 60))
800+
.with_grants(grants)
801+
.with_identity("no-subscribe-participant")
802+
.with_name("no-subscribe-participant")
803+
.to_jwt()
804+
.context("Failed to generate JWT")?;
805+
806+
log::info!("[{}] Connecting with can_subscribe=false", mode.name());
807+
let (room, _events) = connect_room(&url, &token, mode).await?;
808+
809+
assert_eq!(
810+
room.connection_state(),
811+
ConnectionState::Connected,
812+
"Room should be connected even when can_subscribe=false"
813+
);
814+
815+
log::info!("[{}] Test passed - can_subscribe=false connects without timeout!", mode.name());
816+
Ok(())
817+
}
818+
775819
/// Test two sequential reconnect cycles on the same room connection
776820
async fn test_double_reconnect_impl(mode: SignalingMode) -> Result<()> {
777821
let (url, api_key, api_secret) = get_env_for_mode(mode);

0 commit comments

Comments
 (0)