versatica / mediasoup-client

mediasoup client side JavaScript library

Home Page:https://mediasoup.org

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Transport `connectionstatechange` event mismatch

ezioda004 opened this issue · comments

Bug Report

Hi,

From the documentation, transport.on(“connectionstatechange”, fn(connectionState), emits RTCPeerConnectionState. However, mediasoup-client internally listens and emits RTCIceConnectionState.

Your environment

  • Operating system: MacOS
  • Browser version: Chrome 108
  • npm version: 7.24
  • mediasoup version: 3.11.3
  • mediasoup-client version: 3.6.57

Issue description

The issue with this comes during disconnection. Suppose, you turn off the internet, connectionstatechange goes from connected -> disconnect and there's still a chance that the connection can go connected again (there's a 10-second window for the next retry). If the retry is unsuccessful, RTCPeerConnectionState emits failed event whereas RTCIceConnectionState is still in disconnect state so transport never emits the failed event.
For ice restarts, I'm waiting for this failed state, rather than disconnect state because of the auto retry.
Now, since mediasoup-client only emits RTCIceConnectionState on transport, I have to internally listen to this private state via transport.handler._pc.connectionState to find out if the actual connection state and then do an ice restart.

This is mediasoup-client code related to this:

// Listens to RTCIceConnectionState and not RTCPeerConnectionState
this._pc.addEventListener('iceconnectionstatechange', () => {
            switch (this._pc.iceConnectionState) {
                case 'checking':
                    this.emit('@connectionstatechange', 'connecting');
                    break;
                case 'connected':
                case 'completed':
                    this.emit('@connectionstatechange', 'connected');
                    break;
                case 'failed':
                    this.emit('@connectionstatechange', 'failed');
                    break;
                case 'disconnected':
                    this.emit('@connectionstatechange', 'disconnected');
                    break;
                case 'closed':
                    this.emit('@connectionstatechange', 'closed');
                    break;
            }
        });

Wanted to know if this is intentional. If so, then the documentation should reflect the actually used state. Having the transport emit the RTCPeerConnectionState would be useful for the above case mentioned instead of listening on the RTCPeerConnection object itself.

At the time this code was written majority of browsers didn't implement connectionState but just iceConnectionState. Wondering if we should listen for and expose both events or just connectionState.

I think exposing both events makes sense, the developer can decide to use them accordingly. However, firefox still doesn't support connectionState.
Also, I'd love to contribute :D (if you decide to add this event).

That's exactly the problem: how to expose an event in a unified way if it don't exist in some browsers , and we cannot break backwards compatibility. Do you know which exact browsers and versions implement connectionState?

Here is the compatibility table:
Screenshot 2022-12-07 at 2 15 34 PM

For firefox, could we emit iceConnectionState for connectionState?

Basically for other browsers:
iceConnectionState is emitted when iceconnectionstatechange event occurs, mediasoup emits iceconnectionstatechange
connectionState is emitted when connectionstatechange event occurs, mediasoup emits connectionstatechange

Firefox:
iceConnectionState is emitted when iceconnectionstatechange event occurs, mediasoup emits iceconnectionstatechange and connectionstatechange.

The downside of this approach is mediasoup will be emitting two different events in firefox which are the same.

Also fixing events in other browsers will break backwards compatibility in applications? Right now mediasoup emits connectionstatechange on iceconnectionstatechange, but with the fix it'll emit connectionstatechange on connectionstatechange.

I'll think about this in next days. Thanks a lot.

Suppose, you turn off the internet, connectionstatechange goes from connected -> disconnect and there's still a chance that the connection can go connected again (there's a 10-second window for the next retry). If the retry is unsuccessful, RTCPeerConnectionState emits failed event whereas RTCIceConnectionState is still in disconnect state so transport never emits the failed event.

SoI don't understand this. According to the spec, if the retry fails then both RTCIceConnectionState and RTCPeerConnectionState become "failed":

Description of failed state in RTCIceConnectionState:

The previous state doesn't apply and any RTCIceTransports are in the "failed" state.

Description of failed state in RTCPeerConnectionState:

The previous state doesn't apply and any RTCIceTransports are in the "failed" state or any RTCDtlsTransports are in the "failed" state.

This is: in case the ICE connection retry fails, the peerconnection emits iceconnectionstatechange and its RTCIceConnectionState becomes failed, so Transport in mediasoup-client emits on('connectionState, "failed") and you can do ICE restart.

Am I missing something?

SoI don't understand this. According to the spec, if the retry fails then both RTCIceConnectionState and RTCPeerConnectionState become "failed":

Description of failed state in RTCIceConnectionState:

The previous state doesn't apply and any RTCIceTransports are in the "failed" state.

Description of failed state in RTCPeerConnectionState:

The previous state doesn't apply and any RTCIceTransports are in the "failed" state or any RTCDtlsTransports are in the "failed" state.

This is: in case the ICE connection retry fails, the peerconnection emits iceconnectionstatechange and its RTCIceConnectionState becomes failed, so Transport in mediasoup-client emits on('connectionState, "failed") and you can do ICE restart.

Am I missing something?

Okay, so it seems like an issue with Chrome.

Steps to reproduce this bug is simple:

  1. Create a transport.
  2. Create a producer.
  3. Verify the transport is connected and producer is producing track.
  4. Disconnect from the internet.
  5. After disconnecting, 10 secs later iceconnectionstate and connectionstate changes to disconnected. Attaching RTCPeerConnection details below:

Screenshot 2022-12-09 at 2 41 28 PM

  1. After 20 secs post disconnecting from the internet and 10 secs after disconnected state, iceconnectionstate remains disconnected whereas connectionstate changes to failed.

Screenshot 2022-12-09 at 2 41 44 PM

This is the timeline of the states from chrome://webrtc-internals
Screenshot 2022-12-09 at 2 40 51 PM

I tried this in safari as well, there the iceconnectionstate and connectionstate matches with disconnect and failed respectively:
Screenshot 2022-12-09 at 3 02 46 PM

Further digging down, in Chrome, it seems RTCIceTransport and RTCDtlsTransport are not in failed state when connectionstate is in failed state.

Screenshot 2022-12-09 at 3 35 49 PM

So seems like the issue is with Chrome either having a bug or not following the spec?

This is: in case the ICE connection retry fails, the peerconnection emits iceconnectionstatechange and its RTCIceConnectionState becomes failed, so Transport in mediasoup-client emits on('connectionState, "failed") and you can do ICE restart

I have to handle iceRestarts in both the cases now (disconnected and failed), because failed event doesnt get triggered in Chrome but does in Safari. Adding a snippet for someone who stumbles upon in the future:

this.producerTransport.on('connectionstatechange', (connectionState) => {
        console.log('createSendTransport::connectionStateChange', connectionState, this.producerTransport);

        switch (connectionState) {
          case 'connected':
            console.log('createSendTransport::connectionstatechange', 'connected');
            break;
          case 'disconnected':
            setTimeout(() => {
              if ((this.producerTransport.handler as any)._pc.connectionState as any === 'failed') {
                console.log('connectionStateChange::failed', (this.producerTransport.handler as any)._pc);
                this.transportRestartIce(this.producerTransport);
              }
            }, 15000);
            break;
          case 'failed':
            this.transportRestartIce(this.producerTransport);
            break;
        }
      });

Side note, found a discourse forum post with the same issue, they are also using chrome.

If this is a bug in Chrome (so in libwebrtc) please report the issue in their trackers.

Other than that, I'd really like to know if we should make modern handlers (i.e. Chrome74 which implements pc.peerConnectionState) listen for peerconnectionstatechange instead of iceconnectionstatechange. AFAIU from above rationale that would basically fix the issue without introducing any regression and without breaking the API surface (no breaking changes), am I right?

Am I correct to assume you meant handlers that support peerconnectionstatechange? All browsers apart from Firefox?
It should fix the above issue ideally without any breaking change, but at the same time, I also wonder, if some application is relying on this broken behavior in Chrome, cant comment much on that since we don't know when this bug got introduced or if it has been there since the inception.

But as of right now, listening to peerconnectionstatechange instead of iceconnectionstatechange will fix the issue in Chrome.

Am I correct to assume you meant handlers that support peerconnectionstatechange?

Yes, sorry.

All browsers apart from Firefox?

Old versions of other browser do not support it either, but anyway.

Done in 3134822 and released in 3.6.67. Thanks.

Thanks, I'll test it out!