Repository navigation
Match publisher answers to offers by offer id - #1262
Open
hiroshihorie wants to merge 2 commits into
Open
hiroshihorie wants to merge 2 commits into
hiroshihorie wants to merge 2 commits into
Conversation
|
Caution Breaking change detected without major changeset
If this is intentional, please add a changeset with |
Offers and answers were sent without SessionDescription.id, so the transport could only tell whether an answer was still wanted from the signaling state. An answer to an offer that an ICE restart or a newer offer replaced arrives in have-local-offer like the one that is wanted, so it was applied to the wrong offer and the real answer was then dropped as a duplicate. Each offer now gets an id starting at 1, sent with the offer. The server echoes it on its answer, and an answer whose id is not the latest offer's is ignored. A numbered answer is a duplicate once its offer's answer has been applied, and the in-flight claim is taken before any await. Answers from servers that send no id keep the signaling state fallback from the duplicate answer fix. An offer superseded while it was being built is not sent. Subscriber answers echo the server's offer id, as the JS, Swift and Android SDKs do.
hiroshihorie
force-pushed
the
hiroshi/offer-ids
branch
from
October 27, 2026 09:27
622cd92 to
e953b94
Compare
hiroshihorie
marked this pull request as ready for review
October 27, 2026 09:31
hiroshihorie
requested review from
changt,
cloudwebrtc and
xianshijing-lk
as code owners
October 27, 2026 09:31
createAndSendOffer checks for a waiting offer across an await, so two overlapping calls could both pass the check and send two offers. The offer ids then judged the answer to the first one stale, and if the second call failed after taking its id, nothing the server answered matched the latest id until a reconnect. A lock around createAndSendOffer, as the JS SDK has, makes the second call wait. It then finds the first offer waiting for its answer and defers to renegotiate like any other overlapping change. The check that dropped a superseded offer is no longer needed.
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-up to #1257, which added the signaling-state check this PR keeps as the fallback for servers that send no offer id.
The problem
The Flutter SDK sends offers and answers without
SessionDescription.id, so the publisher transport can only use the signaling state to decide whether an answer is still wanted. That works for a duplicate that lands instable(#1257), but not for an answer to an offer that was already replaced.Example: the transport sends offer 2, and before its answer arrives an ICE restart sends offer 3. The answer to offer 2 then arrives in
have-local-offer, the same state the answer to offer 3 will find. It gets applied to the restart offer, and the real answer to offer 3 is dropped as a duplicate.The same thing happens to a duplicate of answer N that arrives while answer N is being applied. Answers are handled one at a time, and handling answer N also sends the deferred offer N+1. So the copy is only processed once offer N+1 is waiting, and it gets applied to offer N+1.
JS (livekit/client-sdk-js#1547, livekit/client-sdk-js#2114), Swift (livekit/client-sdk-swift#845) and Android (livekit/client-sdk-android#813) already match answers to offers by id. The server supports this since livekit/protocol#1105: it echoes the client's offer id on its answer and numbers its own offers.
The fix
onOffer.await, as JS does.createAndSendOfferholds a lock, like JS'sofferLock. A call that overlaps another waits for it, finds that offer waiting for its answer and defers torenegotiateinstead of sending a second offer.SignalOfferEventandSignalAnswerEventcarry the id, and subscriber answers echo the server's offer id.Verification
flutter test: 475 pass.flutter analyze,dart formatandimport_sorterare clean.transport_negotiate_test.dart. Each one fails when the check it covers is removed: the stale check, the in-flight claim, the answered id, recording before apply, the offer lock, and passing the id toonOffer.livekit-server --dev, with two new scenarios from livekit/e2e-flutter#13 and livekit/e2e-flutter#14:sdp state: answer id mismatchwarnings.