Webrtc-pc: offerToReceive* legacy behaviour spec does not match the behaviour of legacy implementations

Created on 13 Jun 2017  路  22Comments  路  Source: w3c/webrtc-pc

Run this snippet in Chrome or Firefox:

var pc = new RTCPeerConnection();
pc.createOffer({offerToReceiveAudio: true})
.then((offer) => {
  console.log('offer 1', offer.sdp)
  return pc.createOffer({offerToReceiveVideo: true})
 })
 .then((offer) => {
  console.log('offer 2', offer.sdp)
 });

The first offer will have an audio m-line. The second offer will have a video m-line.

Now according to the specification of the legacy offerToReceive* this is not valid since offerToReceive internally creates a transceiver. In the addStream-model createOffer was stateless -- I think the same applies to addTrack.
This is quite a surprising side effect, looks like we managed to specify legacy behaviour that does not match the behaviour of legacy clients.

I do not think the spec should be changed but there should at least be a note explaining this.
I actually like the new behaviour more than having to remember what arguments I used previously.

Needs assignee action PR exists

Most helpful comment

Since this is only around for backwards compatibility, creating a transceiver as a side-effect seems like an acceptable option. It would only negatively affect people who don't do setLocalDescription after createOffer({offerToReceiveAudio: true}).

how many weeks would it take you to get number of "createOffer is called with offerToReceive* a second time with offerToReceive* without a setLocalDescription in between" (assuming you can measure that)?

If createOffer({offerToReceiveAudio: true}) creates a transceiver, then to address this we'd just need to change:

the existing localDescription (if any) doesn't contain any sendrecv or recv audio media sections

To:

connection has no non-stopped "sendrecv" or "recvonly" audio transceivers

All 22 comments

Note that the actual impact, i.e. people calling createOffer without setLocalDescription is very low.

So, in the new transceiver based spec, isn't createOffer stateless? If I properly understand, it's not stateless because it creates a transceiver, so the app can set transceiver's parameters and then... createOffer + setRemoteDescription?
Everything around transceivers is crazy. Death to SDP.

Whether createOffer internally creates a transceiver is not very clear from the spec language IMHO:

"When this is given a non-false value, no outgoing track of type "audio" is attached to the PeerConnection, and the existing localDescription (if any) doesn't contain any sendrecv or recv audio media sections, createOffer() will behave as if addTransceiver("audio") had been called once prior to the createOffer() call. "

In the narrowest interpretation, this statement only modifies createOffer's behavior: to act as it would have acted had addTransceiver() been previously called. This to me does not say it acts as addTransceiver.

That said, it raises questions about the state of things after createOffer(). Without a transceiver, there's no sender nor receiver, and no tracks.

@ibc The new createOffer is stateless. It's the old legacy createOffer({offerToReceiveAudio: true}) that (probably) isn't, and probably needs clarifying language that it does indeed implicitly create a transceiver as a kludge.

I think we need to figure out when state is created and modified and if that has changed in the evolution from the streams model to the transceiver model.

When I say that in the addStreams model createOffer is stateless I mean that it does not modify the state on the peerconnection object. I think you mean that the offer that is created carries state. Two sides of the same coin. This is somewhat consistent with Chrome's behaviour when you do this

var pc = new RTCPeerConnection();
pc.createOffer({offerToReceiveAudio: true})
.then(offer => {
  console.log(offer.sdp)
  return pc.createOffer({offerToReceiveAudio: true});
})
.then(offer => {
  console.log(offer.sdp)
});

the ice-ufrag and ice-pwd change. Note that Firefox behaves slightly different and does not change the ice-ufrag/ice-pwd. Both implementations use the same fingerprint which suggests that createOffer does not create a certificate but it is created earlier (as the spec suggests for the constructor).

Only once you call setLocalDescription with an offer the peerconnection future calls to createOffer will return the same ufrag/pwd.
In the ORTC-peerconnection shim I created RTPSenders in createOffer as well. Which might have been wrong, this shows another interpretation which seems to work.

My interpretation of createOffer in the transceivers model is that it merely dumps the internal state of the peerconnection into a blob of SDP. Roughly like like:

let sdp = createSessionPart()
pc.transceivers.forEach(transceiver => {
   sdp += generateMediaSection(transceiver);
})
return {type: offer, sdp}

The transceivers are created by createTransceiver or addTrack.

The problem we have now is that offerToReceive* not only dumps state but potentially modifies it.
As jib says, "will behave as if addTransceiver("audio") had been called once" leaves a bit of leeway. I am interpreting it as "call addTransceiver", i.e.

if (offerToReceiveAudio)
  pc.createTransceiver(audio); // adds a audio transceiver to pc.transceivers
let sdp = createSessionPart();
pc.transceivers.forEach(transceiver => {
   sdp += generateMediaSection(transceiver);
})
return {type: offer, sdp};

which is very convenient and straightforward to implement. Also it does not create objects with a limited lifetime that are only loosely bound to the peerconnection. But it does not act like the legacy implementation.

To make things worse consider this assuming that offerToReceiveAudio does not call addTransceiver but creates some temporary transceiver:

var pc = new RTCPeerConnection();
let r1;
let r2;
pc.createOffer({offerToReceiveAudio: true})
.then(() => {
  r1 = pc.getReceivers()[0];
  return pc.createOffer({offerToReceiveAudio: true});
})
.then(() => {
  r2 = pc.getReceivers()[0];
});

Two questions:
1) is r1 set? I.e. does the call to getReceivers() return anything? http://w3c.github.io/webrtc-pc/#dfn-collecttransceivers has no provisions for any temporary transceivers)
2) If the answer to (1) is yes, does r1 === r2 hold, i.e. is the same object returned?

I checked (1) in both Chrome (beta with experimental web platform features flag) and Firefox.

var pc = new RTCPeerConnection(); pc.createOffer({offerToReceiveAudio: true})
.then(function(offer) { console.log(pc.getReceivers()); })

both return an empty array (in Chrome this might be a result of "receivers+planb" which @henbos mentioned to me once)

In response to question 1. at the end of https://github.com/w3c/webrtc-pc/issues/1383#issuecomment-308647050: r1 should not be set.

I agree there may be some unclear things in the spec here, personally I find it helpful to read JSEP at the same time...

JSEP unfortunately does not cover offerToReceive anymore. From 5.2.1:

The next step is to generate m= sections, as specified in [RFC4566] Section 5.14.
An m= section is generated for each RtpTransceiver that has been added to the 
PeerConnection, excluding any stopped RtpTransceivers. This is done in the order 
the RtpTransceivers were added to the PeerConnection.

This suggests to me that createOffer operates on the pc state and we can not generate an m= line without a transceiver.

One way to wiggle around this is to say that createOffer({offerToReceive}) calls the createOffer algorithm with a set of transceivers which in this case does not match the pcs set of transceivers.
And then use a internal slot for the last offer created and its set of transceivers (potentially just the extra transceivers not part of the normal state). Calls to SLD would then act upon that set.

Such a rabbithole...

I remember a not too distant past when we had no 'offerToReceive' in the spec :)

The alternative would be to let createOffer({offerToReceiveAudio}) actually create a transceiver.
This would mean that we let go of strict legacy compat but only in a very limited and hopefully rather theoretical case (I think we can satisfy @ibc by giving him proper RTCRtpReceiver.getCapabilities()).

@foolip how many weeks would it take you to get number of "createOffer is called with offerToReceive* a second time with offerToReceive* without a setLocalDescription in between" (assuming you can measure that)?

Since this is only around for backwards compatibility, creating a transceiver as a side-effect seems like an acceptable option. It would only negatively affect people who don't do setLocalDescription after createOffer({offerToReceiveAudio: true}).

how many weeks would it take you to get number of "createOffer is called with offerToReceive* a second time with offerToReceive* without a setLocalDescription in between" (assuming you can measure that)?

If createOffer({offerToReceiveAudio: true}) creates a transceiver, then to address this we'd just need to change:

the existing localDescription (if any) doesn't contain any sendrecv or recv audio media sections

To:

connection has no non-stopped "sendrecv" or "recvonly" audio transceivers

@foolip how many weeks would it take you to get number of "createOffer is called with offerToReceive* a second time with offerToReceive* without a setLocalDescription in between" (assuming you can measure that)?

For any use counter we add now, we'd have numbers from the stable channel in September.

The solution proposed by @fippo in https://github.com/w3c/webrtc-pc/issues/1383#issuecomment-308776600 sounds reasonable to me.

I guess it would create a new transceiver for every call to createOffer with the option set, but that's seems like something that we could live with.

Related to #1361.

I guess it would create a new transceiver for every call to createOffer with the option set, but that's seems like something that we could live with.

@stefhak It only creates one if one does not already exist, judging from existing wording, so it is self-limiting.

Create offer has never been stateless - it has always reserved resources.

Create offer has never been stateless - it has always reserved resources.

Which ones?

On everything it reserves ports, on some implementations with hardware codecs, it reservers the hardware codec.

On everything it reserves ports, on some implementations with hardware codecs, it reservers the hardware codec.

No. CreateOffer() does not allocate ports. Only when calling setLocalDescription() one would start to allocate ICE resources. With the exception of the new ICE candidate pool. In that case you need to start allocating ICE resources at the time of createOffer().

The createOffer steps say "If a system has limited resources (e.g. a finite number of decoders), createOffer needs to return an offer that reflects the current state of the system, so that setLocalDescription will succeed when it attempts to acquire those resources. The session descriptions MUST remain usable by setLocalDescription without causing an error until at least the end of the fulfillment callback of the returned promise."

How would createOffer pull that off without any state?

1430 has been merged, and my reading is that we with that can close this Issue.

Was this page helpful?
0 / 5 - 0 ratings