Skip to content

Codec-changing re-INVITE gets 200 OK with stale SDP #766

Description

@LukasKne

Hi, we ran into this on an outbound carrier call and I think the current
re-INVITE handling has a gap.

The initial exchange selected PCMU. Right after the ACK, the carrier sent an
in-dialog re-INVITE offering only PCMA. LiveKit answered 200 OK with the
cached PCMU SDP from the initial exchange and kept the media pipeline on PCMU.
That left no common audio codec between the new offer and the answer, and the
call had no usable return audio.

Simplified SDP sequence:

# Initial LiveKit offer
m=audio 10820 RTP/AVP 9 0 8 101
a=rtpmap:0 PCMU/8000
a=rtpmap:8 PCMA/8000

# Initial peer answer
m=audio 45832 RTP/AVP 0 101
a=rtpmap:0 PCMU/8000

# Peer re-INVITE
m=audio 45832 RTP/AVP 8 96
a=rtpmap:8 PCMA/8000

# LiveKit 200 OK
m=audio 10820 RTP/AVP 0 101
a=rtpmap:0 PCMU/8000

The last answer contains PCMU even though the re-INVITE offered only PCMA.
RFC 3264 section 6.1 requires a sendrecv answer to contain at least one codec
from the offer:

https://www.rfc-editor.org/rfc/rfc3264.html#section-6.1

I checked current main at
179b2574d8e6b97306855cf0c8628b17f53c3e92. The re-INVITE path still parses the
new SDP only to update the RTP address, then returns the old local SDP:

  • sip/pkg/sip/inbound.go

    Lines 323 to 333 in 179b257

    func updateRemoteFromSDP(media *MediaPort, log logger.Logger, codecs *msdk.CodecSet, body []byte) {
    if len(body) == 0 || media == nil {
    return
    }
    desc, err := sdp.ParseWith(codecs, body)
    if err != nil {
    log.Warnw("failed to parse re-INVITE SDP, RTP destination not updated", err)
    return
    }
    media.UpdateRemote(desc.Addr)
    }
  • sip/pkg/sip/inbound.go

    Lines 408 to 418 in 179b257

    if s.cli != nil { // Process reinvite for existing outbound calls
    oc := s.cli.getActiveCall(cc.ID())
    newCSeq := cc.InviteCSeq()
    if oc != nil && oc.cc != nil && oc.cc.InviteCSeq() < newCSeq {
    localSDP := oc.cc.LocalSDP()
    if len(localSDP) != 0 {
    oc.log.Infow("accepting reinvite", "content-length", req.ContentLength(), "cseq", cc.InviteCSeq())
    oc.updateRemoteFromSDP(sdpBodyFromRequest(req))
    oc.cc.RecordInvite(newCSeq)
    cc.AcceptAsKeepAlive(localSDP)
    return nil

I also added a small regression test locally. Before the guard, LiveKit
returned 200; after checking that the currently negotiated codec and payload
types are still offered, the same re-INVITE returns 488 Not Acceptable Here
and leaves the existing RTP destination untouched.

I think either behavior below would be valid:

  1. Fully renegotiate the codec and update the media pipeline before answering
    with matching SDP; or
  2. Reject a codec/payload change with 488 and keep the established session.

What should not happen is a successful answer containing a codec that was not
in the offer.

This looks related to #661 and #728. The RTP destination update from #728 works,
but the codec change mentioned in #661 still appears to be unhandled.

If you want I can also fix it myself and create a Pull Request for this.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions