Skip to content

Establish early media with 183 Session Progress #211

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
matthieusieben opened this issue Aug 20, 2015 · 11 comments
Closed

Establish early media with 183 Session Progress #211

matthieusieben opened this issue Aug 20, 2015 · 11 comments

Comments

@matthieusieben
Copy link

rfc3261 requires the UAC to support and process 183 messages.

However, SIP.js only supports early media via the 100rel tag option. 183 messages are simply ignored.

@matthieusieben
Copy link
Author

matthieusieben commented Aug 20, 2015

Here is my current workaround:

outgoingInviteSession
  .on('progress', function (response) {
    // InviteClientContext#receiveInviteResponse
    if (response.status_code === 183 && response.body && this.hasOffer && !this.dialog) {
      if (!response.hasHeader('require') || response.getHeader('require').indexOf('100rel') === -1) {
        if (this.mediaHandler.hasDescription(response)) {
          // Mimic rel-100 behavior

          if (!this.createDialog(response, 'UAC')) { // confirm the dialog, eventhough it's a provisional answer
            return
          }

          // this ensures that 200 will not try to set description
          this.hasAnswer = true

          // This ensures that when you receive a 1XX after the 183, no
          // earlyDialog are created once the dialog has been confirmed.
          //
          // Without this piece of code:
          //  1. upon reception of a 1XX a new earlyDialog is created and a
          //     PRACK is send (CSEQ is incremented in this new dialog object).
          //  2. at some point we receive the 200.
          //  3. when sending a SIP INFO it will use the CSEQ from the confirmed
          //     dialog. This CSEQ was already used when sending the PRACK.
          //     Asterisk ignores this message since it thinks it is a
          //     retransmit.
          this.dialog.pracked.push(response.getHeader('rseq'))

          // This is different from the behaviour in SIP.JS. Due to concurrency
          // issues, we need to consider that we already have STATUS_EARLY_MEDIA
          // before actually setting the description.
          //
          // @hack: https://github.com/onsip/SIP.js/issues/242
          this.status = Session.C.STATUS_EARLY_MEDIA

          // Mute local streams since we are going to establish early media.
          this.mute()

          this.mediaHandler
            .setDescription(response)
            .catch((reason) => {
              this.logger.warn(reason)
              this.failed(response, C.causes.BAD_MEDIA_DESCRIPTION)
              this.terminate({ status_code: 488, reason_phrase: 'Bad Media Description' })
            })
        }
      }
    }
  });

Have a look at #242 if you do this.

@etamme
Copy link
Collaborator

etamme commented Aug 20, 2015

Because exchanging media requires ICE and DTLS negotiation which is done in both the signalling and in the rtp, there is no way to establish an audio stream before a full offer answer has been exchanged.

SIP.js supports early media via an offer in the 183 and an answer in a PRACK, which as you said does rely on RFC3262 reliable transmission of provisional responses, aka 100rel.

There is simply no way to set up media in a webrtc session without a complete offer answer - it is literally not possible.

@matthieusieben
Copy link
Author

I understand how WebRTC and the SDP exchange work. What I want to achieve here is to establish an early media audio stream for an outgoing call. Consider the following scenario (from the UAC's perspective):

  1. Send invite containing localOffer
  2. Received 180 Ringing from remote party (start playing ringtone)
  3. Receive 183 Progress containing remoteOffer (Let ICE begin. Mute local stream)
  4. ICE Connected: Play early media from remote party
  5. Receive 200 (unmute local stream, remote stream is no longer considered as early media)

This workflow is valid according to rfc3261 and does not require rfc3262 (100rel) in order to establish early media.
Note that this is exactly what the code from my previous comment does. And it works as I was able to test it on my PBX.

@wakamoleguy
Copy link
Contributor

As @etamme said, we cannot support this in SIP.js. I'll clarify a little bit as to why, though, because there are a lot of reasons combining to make this scenario happen.

  • You're misinterpreting the RFC section you linked. 183 Session Progress behavior is described in Section 21.1.5, and it doesn't actually mention early media.

21.1.5 183 Session Progress

The 183 (Session Progress) response is used to convey information
about the progress of the call that is not otherwise classified. The
Reason-Phrase, header fields, or message body MAY be used to convey
more details about the call progress.

Early media is simply a common behavior associated with 183s. The requirement for processing 183 responses is actually about the same as process 180 Ringing responses. Early media is (or can be) a side effect of progress responses with SDP in them, but that actually has nothing to do with the status code of the response.

  • This is the only reference I can find in RFC 3261 referring to an answer before the first reliable response. It doesn't actually say how to process it, just that it must be treated as final for that branch.

If the initial offer is in an INVITE, the answer MUST be in a
reliable non-failure message from UAS back to UAC which is
correlated to that INVITE. For this specification, that is
only the final 2xx response to that INVITE. That same exact
answer MAY also be placed in any provisional responses sent
prior to the answer. The UAC MUST treat the first session
description it receives as the answer, and MUST ignore any
session descriptions in subsequent responses to the initial
INVITE.

To me, this means that once a reliable response has been received, we should use the first SDP received. Granted, we are throwing away the first answer and assuming the reliable response is the same (as the RFC specifies), but I don't see anything saying that the answer must be immediately acted upon.

  • As @etamme described, WebRTC is not as flexible as other endpoints when it comes to media rendering. When we place an offer, we are restricted to one PeerConnection with that information. This means that we can only handle one branch of the call providing an answer. If multiple branches reply, we can only set up media on one of them. (Note: We get around this when we send INVITEs without SDP, as we can spin up a new PeerConnection for every offer we receive.)
  • Since accepting the offer in a non-reliable provisional response breaks call forking, we've decided it needs to be opt in. 100rel is a standards-compliant way to do that, and we don't feel like our default behavior is against the RFC.

So, while I understand that it would be valuable, supporting early media without 100rel is just not something that we are able to support. Based on technical constraints, the tradeoffs of breaking other working scenarios are just not worth it.

I'm glad to see that there is a workaround, however, (although I understand it is not ideal to monkey-patch the library). I'll probably end up referring back to this Issue later when other people ask about it.

Thanks,

-Will

@fatihorhan
Copy link

fatihorhan commented Nov 1, 2017

UPDATE:
It turns out this code was OK, we just didn't attach media streams early enough. Leaving rest of the comment as is in case it helps someone.

Hello
We were happily using @matthieusieben 's workaround for 0.7.8 but after updating to 0.8.2, it seem to no longer work.

Trying to fix, we changed all this.mediaHandler to this.sessionDescriptionHandler.

And
if (this.mediaHandler.hasDescription(response)) {
line to
if (this.sessionDescriptionHandler.hasDescription(response.getHeader('Content-Type'))) {

and
.setDescription(response)
line to
.setDescription(response.body)

Your workaround seem to be based on this code:

SIP.js/src/Session.js

Lines 1716 to 1738 in 560f5b3

if(!this.sessionDescriptionHandler.hasDescription(response.getHeader('Content-Type'))) {
this.acceptAndTerminate(response, 400, 'Missing session description');
this.failed(response, SIP.C.causes.BAD_MEDIA_DESCRIPTION);
break;
}
if (!this.createDialog(response, 'UAC')) {
break;
}
this.hasOffer = true;
this.sessionDescriptionHandler.setDescription(response.body, this.sessionDescriptionHandlerOptions, this.modifiers)
.then(this.sessionDescriptionHandler.getDescription.bind(this.sessionDescriptionHandler, this.sessionDescriptionHandlerOptions, this.modifiers))
.then(function onSuccess(description) {
//var localMedia;
if(session.isCanceled || session.status === C.STATUS_TERMINATED) {
return;
}
session.status = C.STATUS_CONFIRMED;
session.hasAnswer = true;
session.emit("ack", response.transaction.sendACK({body: description}));
session.accepted(response);
})

So we checked there to see if new things needed to be added. Alas, we couldn't make it work. Anybody have some pointers what is missing?

Here is our modified code (coffeescript):

session.on 'progress', (response)=>
  if (response.status_code == 183 && response.body && session.hasOffer && !session.dialog)
    if (!response.hasHeader('require') || response.getHeader('require').indexOf('100rel') == -1)
      if session.sessionDescriptionHandler.hasDescription(response.getHeader('Content-Type'))
        return if !session.createDialog(response, 'UAC')

        session.hasAnswer = true
        console.warn(response)
        session.dialog.pracked.push(response.getHeader('rseq'))
        session.status = SIP.Session.C.STATUS_EARLY_MEDIA
        # session.mute()
        session.sessionDescriptionHandler.setDescription(response.body, session.sessionDescriptionHandlerOptions, session.modifiers).catch (reason)=>
          session.logger.warn(reason)
          session.failed(response, C.causes.BAD_MEDIA_DESCRIPTION)
          session.terminate({ status_code: 488, reason_phrase: 'Bad Media Description'})

@zusrut
Copy link
Contributor

zusrut commented Apr 3, 2019

UPDATE:
It turns out this code was OK, we just didn't attach media streams early enough. Leaving rest of the comment as is in case it helps someone.
.........

stil working with 0.13.7
just status_code -> statusCode
thanks

@FubinSama
Copy link

How can I get early media in sip.js-0.15.6???

@zusrut
Copy link
Contributor

zusrut commented Nov 30, 2019

still works - just remove methods and properties not existing anymore

@FubinSama
Copy link

I'm a newbie. Could u give me a demo??? Thank u very much

@stefanofavaro
Copy link

still works - just remove methods and properties not existing anymore

Do you have an example ? Is it working also with 0.20 ? Thanks.

@a4business
Copy link

a4business commented Jan 24, 2023

This still works on sip-0.15.11.js , but this is last version of SIPjs on old API, newer SIPjs versions should use another solution for early media.

It includes a fix #242 (comment) , where Early media causes problem if 200 comes fast.

session.on('progress', function (response) {
 if (response.statusCode == 183 && response.body && session.hasOffer )
   if (!response.hasHeader('require') || response.getHeader('require').indexOf('100rel') == -1)
      if ( session.sessionDescriptionHandler.hasDescription(response.getHeader('Content-Type')) ) {
       
         session.status = SIP.Session.C.STATUS_EARLY_MEDIA;        
         document.getElementById('outbound_audio').pause();	    // Stop Ringing signal //
         let i = 1,
         clearTimer;
 		setTimeout(function check() {
 			i++;
 			clearTimer = setTimeout(check, 10);
 			if ( session.hasAnswer || i > 14) {
				if ( session.hasAnswer ) {
					clearTimeout(clearTimer);
 				} else if ( i === 15 ) {
 					clearTimeout(clearTimer);
 					session.sessionDescriptionHandler.setDescription(response.body).catch((exception) => {
 						session.logger.warn(exception);
 						session.failed(response, 'Bad Media Description');
 						session.terminate({
 							statusCode: 488,
 							reason_phrase: 'Bad Media Description'
 						});
 					});
 				}
 			}
 		}, 10);

      }
  });

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

8 participants