Skip to content

fix(llc): renegotiate unacknowledged transceivers - #1288

Merged
Brazol merged 8 commits into
mainfrom
fix/peer-connection-fixes
Jul 30, 2026
Merged

fix(llc): renegotiate unacknowledged transceivers#1288
Brazol merged 8 commits into
mainfrom
fix/peer-connection-fixes

Conversation

@Brazol

@Brazol Brazol commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

This PR fixes an issue where republishing could reuse a cached publisher transceiver without renegotiating, even if the previous SetPublisher negotiation failed. Publisher transceivers are now marked as negotiated only after a successful SFU negotiation. If a cached transceiver was never acknowledged by the SFU republishing reuses it but triggers a new negotiation so the track is properly announced.

Summary by CodeRabbit

  • Bug Fixes
    • Improved handling for republishing/unmuting to ensure required publisher renegotiation occurs and stale cached transceivers aren’t reused.
    • Added more robust rollback/recovery when negotiation or publisher updates fail.
    • Fixed a FormatException caused by non-ASCII client/device info by sanitizing X-Stream-Client header values.
    • Updated signaling stall recovery to use a faster reconnection strategy.
  • Improvements
    • Exposed whether the peer connection is currently reconnecting.
  • Tests
    • Expanded unit test coverage for transceiver negotiation, republishing, unmuting, and reconnect safety.

@Brazol
Brazol requested a review from a team as a code owner July 22, 2026 20:05
@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Cached publisher transceivers now track SFU negotiation acknowledgement. Publish and unmute flows request renegotiation for unacknowledged active transceivers, while renegotiation failures roll back explicitly and stalled publisher recovery uses fast reconnect.

Changes

Transceiver renegotiation tracking

Layer / File(s) Summary
Negotiation acknowledgement state and tests
packages/stream_video/lib/src/webrtc/transceiver_cache.dart, packages/stream_video/test/src/webrtc/transceiver_cache_test.dart
Cached transceivers default to negotiated: false; matching announced active tracks are marked negotiated, with coverage for in-flight negotiation and replacement cases.
Publisher reuse and unmute recovery
packages/stream_video/lib/src/webrtc/rtc_manager.dart, packages/stream_video/lib/src/webrtc/peer_connection.dart, packages/stream_video/test/src/webrtc/rtc_manager_unmute_renegotiation_test.dart
Audio, video, and unmute paths trigger renegotiation for unacknowledged transceivers, except during reconnect; tests cover acknowledged, inactive, and reconnecting states.
Renegotiation error handling and reconnect fallback
packages/stream_video/lib/src/call/session/call_session.dart, packages/stream_video/test/src/call/session/call_session_reconnect_safety_test.dart, packages/stream_video/CHANGELOG.md
Renegotiation failures now roll back and return explicit errors, acknowledgement occurs after a successful remote answer, stalled recovery uses fast reconnect, and fixes are documented.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant RtcManager
  participant CallSession
  participant SFU
  participant TransceiverManager
  RtcManager->>CallSession: request publisher renegotiation
  CallSession->>SFU: send publisher offer
  SFU-->>CallSession: return publisher answer
  CallSession->>TransceiverManager: mark active tracks negotiated
Loading

Possibly related PRs

Suggested reviewers: renefloor, xsahil03x

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description is too brief and misses the required Goal, Implementation details, Testing, and checklist sections from the template. Rewrite the PR description to include the template sections, especially Goal, Implementation details, Testing, and the contributor/reviewer checklists.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly matches the main change: renegotiating unacknowledged cached publisher transceivers.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/peer-connection-fixes

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Jul 22, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 40.00000% with 30 lines in your changes missing coverage. Please review.
✅ Project coverage is 11.35%. Comparing base (cd8d47f) to head (2a0e611).

Files with missing lines Patch % Lines
...tream_video/lib/src/call/session/call_session.dart 5.55% 17 Missing ⚠️
...kages/stream_video/lib/src/webrtc/rtc_manager.dart 52.17% 11 Missing ⚠️
...s/stream_video/lib/src/webrtc/peer_connection.dart 0.00% 1 Missing ⚠️
...stream_video/lib/src/webrtc/transceiver_cache.dart 87.50% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1288      +/-   ##
==========================================
+ Coverage   11.22%   11.35%   +0.13%     
==========================================
  Files         686      686              
  Lines       50361    50401      +40     
==========================================
+ Hits         5652     5724      +72     
+ Misses      44709    44677      -32     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/stream_video/lib/src/call/session/call_session.dart`:
- Line 1145: Move rtcManager?.transceiversManager.markNegotiated() into the
successful pc.setRemoteAnswer() path, ensuring it is not executed when remote
answer application throws or fails. Preserve the existing recovery behavior for
failed negotiation so later republishing can force renegotiation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 5afa25b4-a2c3-495e-8e98-d959af656ff2

📥 Commits

Reviewing files that changed from the base of the PR and between d0b3a62 and bdbefda.

📒 Files selected for processing (5)
  • packages/stream_video/CHANGELOG.md
  • packages/stream_video/lib/src/call/session/call_session.dart
  • packages/stream_video/lib/src/webrtc/rtc_manager.dart
  • packages/stream_video/lib/src/webrtc/transceiver_cache.dart
  • packages/stream_video/test/src/webrtc/transceiver_cache_test.dart

Comment thread packages/stream_video/lib/src/call/session/call_session.dart Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/stream_video/lib/src/call/session/call_session.dart (1)

1140-1147: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Rollback after a failed remote answer.

A failed setRemoteAnswer returns without rolling back the local offer, leaving the publisher potentially stuck in have-local-offer; the subsequent forced renegotiation can then fail instead of recovering.

Proposed fix
           if (ansResult is! Success<void>) {
             _logger.w(
               () => '[negotiate] `#setRemoteAnswer`; failed: $ansResult',
             );
+            await pc.rollbackLocalDescription();
             return Result<void>.error(
               'Failed to set remote answer: ${ansResult.getErrorOrNull()}',
             );
           }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/stream_video/lib/src/call/session/call_session.dart` around lines
1140 - 1147, Update the failed setRemoteAnswer branch in the negotiation flow to
roll back the local offer before returning the error result. Use the existing
RTCPeerConnection rollback mechanism and ensure it completes before the return,
while preserving the current warning and error propagation behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@packages/stream_video/lib/src/call/session/call_session.dart`:
- Around line 1140-1147: Update the failed setRemoteAnswer branch in the
negotiation flow to roll back the local offer before returning the error result.
Use the existing RTCPeerConnection rollback mechanism and ensure it completes
before the return, while preserving the current warning and error propagation
behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 6814a368-a3ad-4c75-9b9c-1771e51a65ab

📥 Commits

Reviewing files that changed from the base of the PR and between bdbefda and adcadc7.

📒 Files selected for processing (5)
  • packages/stream_video/lib/src/call/call.dart
  • packages/stream_video/lib/src/call/session/call_session.dart
  • packages/stream_video/lib/src/webrtc/peer_connection.dart
  • packages/stream_video/lib/src/webrtc/rtc_manager.dart
  • packages/stream_video/test/src/webrtc/rtc_manager_unmute_renegotiation_test.dart

Comment thread packages/stream_video/lib/src/call/session/call_session.dart

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/stream_video/lib/src/webrtc/transceiver_cache.dart (1)

26-26: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Reset acknowledgement when the cached media track changes.

A previously acknowledged entry remains negotiated == true after update(..., track:) replaces its mediaTrack.id. Reuse can then skip renegotiation although the SFU never received the replacement track.

  • packages/stream_video/lib/src/webrtc/transceiver_cache.dart#L26-L26: clear negotiated when update receives a track with a different media-track ID.
  • packages/stream_video/test/src/webrtc/transceiver_cache_test.dart#L168-L182: first mark the old track negotiated, replace it through update, then assert the cached entry becomes unnegotiated.
Proposed fix
 if (track != null) {
+  final mediaTrackChanged =
+      bundle.track.mediaTrack.id != track.mediaTrack.id;
   bundle.track = track;
+  if (mediaTrackChanged) {
+    bundle.negotiated = false;
+  }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/stream_video/lib/src/webrtc/transceiver_cache.dart` at line 26,
Reset the cached entry’s negotiated state in update when the supplied track has
a different media-track ID, so replacement tracks require renegotiation;
preserve the state when the ID is unchanged. In
packages/stream_video/lib/src/webrtc/transceiver_cache.dart:26, update the
negotiated handling for the cache entry, and in
packages/stream_video/test/src/webrtc/transceiver_cache_test.dart:168-182, mark
the original track negotiated, replace it via update, and assert the entry is
unnegotiated.
🧹 Nitpick comments (1)
packages/stream_video/test/src/call/session/call_session_reconnect_safety_test.dart (1)

249-250: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Exercise a real renegotiation failure.

Because the test has no SFU connection, _onRenegotiationNeeded exits before createOffer, rollback, SetPublisher, or setRemoteAnswer. It therefore validates only the disconnected-WebSocket failure, not the cleanup and fallback behavior changed by this PR. Add a connected fake-SFU case that fails after offer creation and asserts rollback plus fast reconnect.

Also applies to: 276-282

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/stream_video/test/src/call/session/call_session_reconnect_safety_test.dart`
around lines 249 - 250, Extend the test covering `_onRenegotiationNeeded` so it
uses a connected fake SFU and reaches the post-`createOffer` failure path.
Configure the fake response to fail during renegotiation, then assert that
rollback and publisher reset occur and that the reconnect request uses the
`fast` mode, rather than only validating the disconnected-WebSocket path.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/stream_video/lib/src/call/session/call_session.dart`:
- Around line 1118-1120: Update the recovery paths in CallSession around the
setRemoteAnswer failure handling and existing rollbackLocalDescription calls to
always attempt rollback when answer application fails, including the path that
currently returns immediately. Do not discard rollback results: propagate or
compose rollback failures so the empty-track path cannot report success when
rollback fails, while preserving successful recovery behavior.

---

Outside diff comments:
In `@packages/stream_video/lib/src/webrtc/transceiver_cache.dart`:
- Line 26: Reset the cached entry’s negotiated state in update when the supplied
track has a different media-track ID, so replacement tracks require
renegotiation; preserve the state when the ID is unchanged. In
packages/stream_video/lib/src/webrtc/transceiver_cache.dart:26, update the
negotiated handling for the cache entry, and in
packages/stream_video/test/src/webrtc/transceiver_cache_test.dart:168-182, mark
the original track negotiated, replace it via update, and assert the entry is
unnegotiated.

---

Nitpick comments:
In
`@packages/stream_video/test/src/call/session/call_session_reconnect_safety_test.dart`:
- Around line 249-250: Extend the test covering `_onRenegotiationNeeded` so it
uses a connected fake SFU and reaches the post-`createOffer` failure path.
Configure the fake response to fail during renegotiation, then assert that
rollback and publisher reset occur and that the reconnect request uses the
`fast` mode, rather than only validating the disconnected-WebSocket path.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d51b9ec1-55ec-4cae-8237-39f9c9ee2084

📥 Commits

Reviewing files that changed from the base of the PR and between adcadc7 and 63b4ca7.

📒 Files selected for processing (5)
  • packages/stream_video/lib/src/call/session/call_session.dart
  • packages/stream_video/lib/src/webrtc/transceiver_cache.dart
  • packages/stream_video/test/src/call/session/call_session_reconnect_safety_test.dart
  • packages/stream_video/test/src/webrtc/rtc_manager_unmute_renegotiation_test.dart
  • packages/stream_video/test/src/webrtc/transceiver_cache_test.dart
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/stream_video/test/src/webrtc/rtc_manager_unmute_renegotiation_test.dart

Comment thread packages/stream_video/lib/src/call/session/call_session.dart

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/stream_video/lib/src/call/session/call_session.dart (1)

1147-1152: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Complete rollback handling on every negotiation failure.

SetPublisher failure and the catch path still discard rollback failures, while setRemoteAnswer failure returns without rolling back. This can leave the peer connection in have-local-offer; the new fast fallback reuses that connection and may remain wedged. Roll back on answer failure and propagate or compose rollback failures in all three paths.

Also applies to: 1155-1163, 1170-1173

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/stream_video/lib/src/call/session/call_session.dart` around lines
1147 - 1152, Update the negotiation flow around setPublisher, setRemoteAnswer,
and its catch handler to always attempt pc.rollbackLocalDescription() on
failure, including the setRemoteAnswer failure path. Do not discard rollback
errors: propagate or compose each rollback failure with the original negotiation
error while preserving the existing error context and return behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@packages/stream_video/lib/src/call/session/call_session.dart`:
- Around line 1147-1152: Update the negotiation flow around setPublisher,
setRemoteAnswer, and its catch handler to always attempt
pc.rollbackLocalDescription() on failure, including the setRemoteAnswer failure
path. Do not discard rollback errors: propagate or compose each rollback failure
with the original negotiation error while preserving the existing error context
and return behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 5020ef38-c08b-493e-b528-8e25fc577ae8

📥 Commits

Reviewing files that changed from the base of the PR and between 63b4ca7 and 2a0e611.

📒 Files selected for processing (2)
  • packages/stream_video/CHANGELOG.md
  • packages/stream_video/lib/src/call/session/call_session.dart
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/stream_video/CHANGELOG.md

@Brazol
Brazol merged commit 1b53a68 into main Jul 30, 2026
20 checks passed
@Brazol
Brazol deleted the fix/peer-connection-fixes branch July 30, 2026 13:56
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