WebRTC developers do not normally bother about an SDP blob. It just works in the initial negotiation, media streams are exchanged, and the details of the format are irrelevant. But once renegotiation happens — adding a track, starting a screen share, performing ICE restart — things break, and you get an error message as if it was meant for somebody else.Failed to set remote answer sdp: The order of m-lines in answerdoesn't match order in offer. Rejecting answer.There are some rules which generate these errors and they are quite simple but they remain hidden until you violate them. Here are a few of them which you should know in advance.Media sections are positional, and their order is permanentAn SDP session description consists of a number of media sections, each of them starts with an m= line. And the order of media sections is important. Once a session is established, then for each offer/answer the nth media section should always match the nth media section of the initial negotiation.They cannot be reordered, cannot be removed from somewhere in the middle. Media sections in an answer should have the same order as in the offer being answered.This is confusing because the rule does not match the behavior of the rest of the API. Transceivers are represented as a collection of objects which can be manipulated in different ways but the media sections have their own special semantics: adding is okay (it always adds them at the end of the list), everything else is illegal.Transceiver order determines media section orderWhen media sections are positional, then the real question is: what determines the position? Order of creation of transceivers.getTransceivers() returns the list of transceivers in their creation order, which then determines the order of the media sections in the SDP. Thus, any nondeterministic creation of transceivers results in nondeterministic SDP.One common mistake here is not obvious at first glance. Any branch of execution in which the order of addTrack() calls does not depend explicitly on surrounding logic but happens because of iterating over a collection of tracks, racing to acquire devices asynchronously, or even conditional statements, which add video tracks only when certain conditions are met, encodes the nondeterministic order right into the session, which can never be changed. The piece of code looks safe enough:// Transceiver order is now whatever iteration order happened to befor (const track of stream.getTracks()) { pc.addTrack(track, stream);}Nothing in your code decided the order, so nothing in your code can assume anything about the order — and after the first negotiation, it is already frozen in the SDP. Add the tracks explicitly, in the order you decide:const [audio] = stream.getAudioTracks();const [video] = stream.getVideoTracks();if (audio) pc.addTrack(audio, stream);if (video) pc.addTrack(video, stream);There are two related behaviors that might come handy to know. Firstly, addTransceiver() always adds a brand new transceiver, while addTrack() might use an existing one with an available media section. Secondly, when a transceiver stops, there are two steps involved: initially, it is still included in getTransceivers() while stop is being negotiated, and only afterwards removed from the set of transceivers, while the media section it used becomes free for future use by some other transceiver.To correlate transceivers among peers, use the mid of the transceiver, not its index in the array.Let the browser build the descriptionBetween the call to createOffer() and setLocalDescription(offer) there's a potential race window. The parameterless version closes it:await pc.setLocalDescription();signaling.send({ description: pc.localDescription });Calling setLocalDescription() without arguments implicitly creates the correct description according to the current signaling state, be it an offer if it is in stable and an answer if it is in have-remote-offer. It leaves no period where you hold onto an obsolete description and no conditional logic in your code for deciding if it is an offer or an answer.It eliminates one class of bugs entirely: modification of the SDP description between creation and use. SDP manipulation is sometimes necessary, but each case of it is a spot where the next browser update will break you, and most SDP manipulations out there are workarounds for APIs that actually exist.InvalidAccessError and OperationError mean different thingsIf setRemoteDescription() fails, the error type gives you which layer broke – and the spec is explicit about that in a way the error message isn't.InvalidAccessError is thrown under certain conditions: the description does not pass the semantics validation specified by the JSEP (RFC 9429, §5.8.3); the connection requires RTCP multiplexing, while the description lacks it; or the description is trying to renegotiate RIDs, which is not allowed. All other cases throw OperationError.Practical interpretation: InvalidAccessError usually indicates incompatibility of the SDP description with the established session — the media section order issues listed above fall into this category. OperationError means something failed outside of the scope. When troubleshooting the renegotiation issue, this knowledge helps greatly to narrow down the scope of possible causes, as opposed to relying on browser-specific error messages.Also applies to testing. When building a test harness for connection recovery experiments, I decided to fail the preflight step on any SDP negotiation failure, media section order issue, or InvalidAccessError in the description validation. Those errors indicate an issue with the harness itself, rather than with the connection being tested. Failure to handle them would contaminate all further results.Both sides offering at onceThe protocol itself does not prevent two parties from making an offer simultaneously. It happens whenever both parties react to some change in a similar manner – network state changes, hardware switching, connection recovery attempt, whatever.The offer/answer model does not provide for concurrent offers. So, one of those must be canceled. Rollback procedure does exactly that:await pc.setLocalDescription({ type: "rollback" });Rollback reverts the connection to stable, removing the outstanding local offer so that the incoming remote offer may be processed.Determining which party is responsible for rollback is the place where the perfect negotiation pattern comes into play: one party is marked polite and backs down when collision happens, the other is marked impolite and disregards it. The designation is arbitrary, provided the two parties disagree. Should your signaling setup be strictly one-sided, where only one party initiates offers, you don't need any of this. Otherwise you do, and the problem will only arise when you have the least interest in adding a new one.The underlying modelThe majority of the rules above follow from a single premise: offer/answer is a state machine working with a shared, sequential structure which is constantly growing and which both parties should agree upon.As soon as this is understood, all the details become rather obvious: media sections cannot be rearranged because their position is their identity. mid field exists because array indices cannot be used as unique identifiers. Rollback exists because there is no valid state for two outstanding offers in the state machine. Parameter-less setLocalDescription() exists because proper description is always calculable from the current state, so delegating the choice to the caller is a waste of effort.Reading one complete SDP text line-by-line will definitely take you twenty minutes, but the next renegotiation problem will surely take much less time to solve.ReferencesW3C, WebRTC: Real-Time Communication in Browsers — setRemoteDescription() exception conditions.Uberti, J., Jennings, C., and Rescorla, E., JavaScript Session Establishment Protocol (JSEP), RFC 9429, IETF, 2024 — §5.8.3, semantics validation.MDN Web Docs, RTCPeerConnection.setLocalDescription() — implicit descriptions.Bruaroey, J-I., Exploring RTCRtpTransceiver, Advancing WebRTC (Mozilla), 2020 — transceiver ordering and media section reuse; stopped-transceiver removal per the current W3C spec (webrtc-pc, "Remove stopped transceivers after negotiation").MDN Web Docs, Perfect negotiation.