w3c / webrtc-extensions

A repository for experimental additions to the WebRTC API
https://w3c.github.io/webrtc-extensions/
Other
58 stars 19 forks source link

ICE improvements: send and prevent ICE check over a candidate pair #209

Open sam-vi opened 3 months ago

sam-vi commented 3 months ago

Background

Following the incremental approach for improving ICE control capabilities, mechanisms are now defined to:

Problem:

With the Application having multiple candidate pairs at its disposal and an ability to switch the transport between them, it needs a way to gather up-to-date information about the available candidate pairs. With this information, the Application can decide the best candidate pair to use for transport at any given time.

Information about candidate pair can be queried through getStats, which contains, among other things, the current and total RTT, and the number of ICE checks sent, received, and responded.

But ICE checks are sent only over the candidate pair actively used for transport at a frequent interval (around once every couple of seconds). ICE checks are sent less frequently over any inactive candidate pairs (once every ~15 seconds). So information about an inactive, alternate candidate pair could be very stale, or indeed the pair may no longer be connected, when the Application wants to switch the transport to it.

On the other hand, the Application may want to avoid sending ICE checks on an inactive candidate pair, eg. on a reliable or a power-sensitive interface.

Proposal

Introduce a new API to allow applications to (at a minimum):

  1. know when the ICE agent is about to send an ICE check, and possibly prevent it
  2. know when an ICE check concludes with either a response or a timeout
  3. send an ICE check on an available candidate pair

There are a couple of options for the shape of such an API. With both options, an ICE check begins with either

Option 1: Linked Promises

This option allows the App to follow the course of an ICE check at each step of the way - from initiation to being sent to the conclusion.

Also, information about each step becomes available as soon as the step concludes, eg. the sentTime is known when the ICE check is sent while the responseTime is known if and when the response is received.

partial interface RTCIceTransport {
  // Send an ICE check. Resolves when the check is actually sent.
  Promise<RTCIceCheckRequest> checkCandidatePair(RTCIceCandidatePair pair);
  // Fired before ICE agent sends an ICE check.
  // Cancellable, unless triggered check or nomination or app initiated.
  attribute EventHandler /* RTCIceCheckEvent */ onicecandidatepaircheck;
}

interface RTCIceCheckEvent : Event {    // Cancellable
  readonly attribute RTCIceCandidatePair candidatePair;
  // Resolves when the check is actually sent. Rejected => send failure.
  readonly attribute Promise<RTCIceCheckRequest> request;
}

interface RTCIceCheckRequest {
  readonly attribute ArrayBuffer transactionId;
  readonly attribute DOMHighResTimeStamp sentTime;
  // Resolves when response is received. Rejected => timeout.
  readonly attribute Promise<RTCIceCheckResponse> response;
}

interface RTCIceCheckResponse {
  readonly attribute DOMHighResTimeStamp receivedTime;
  // No error => success.
  readonly attribute RTCIceCheckResponseError? error;
}

Option 2: Flat events

This option only conveys the beginning and conclusion of an ICE check to the App.

All information gleaned from the ICE check is available together at the conclusion of the check.

The main drawback of this option is that the App doesn't know exactly when an ICE check has been sent in the case of an ICE agent-initiated check.

partial interface RTCIceTransport {
  // Send an ICE check. Resolves when the check is actually sent.
  Promise<undefined> checkCandidatePair(RTCIceCandidatePair pair);
  // Fired before ICE agent sends an ICE check.
  // Cancellable, unless triggered check or nomination or app initiated.
  attribute EventHandler /* RTCIceCandidatePairEvent */ onicecandidatepaircheck;
  // Fired when an ICE check concludes.
  attribute EventHandler /* RTCIceCheckEvent */ onicecandidatepaircheckcomplete;
}

interface RTCIceCheckEvent : Event {
  readonly attribute RTCIceCandidatePair candidatePair;
  readonly attribute ArrayBuffer transactionId;
  readonly attribute DOMHighResTimeStamp sentTime;
  // No receivedTime => timeout.
  readonly attribute DOMHighResTimeStamp? receivedTime;
  // No error => success.
  readonly attribute RTCIceCheckResponseError? error;
}

ICE agent interactions

Both options allow an App to modify ICE check behaviour in the same way. Here is a proposal for how these modifications are handled by the ICE agent:

Prevent an ICE check

At the beginning of an ICE session, or after an ICE restart, the ICE agent goes through the process of forming checklists of candidate pairs and then performing connectivity checks (RFC 8445 section 6.1.4.2). While a data session is in progress, the ICE agent continues to send ICE checks on the active and any other candidate pairs as keeplives (RFC 8445 section 11).

If an App prevents a scheduled ICE check from being sent, the ICE agent takes no further action at the current expiration of the schedule timer, and defers until the timer expires again.

It is possible that at the next timer expiration, the same candidate pair is picked to send a check over. This is expected, and it is up to the App to ensure that either

The App is not permitted to prevent the sending of triggered checks (RFC 8445 section 6.1.4.1), and as a corollary, nominations (RFC 8445 section 8.1.1).

The API also does not permit the App from preventing a response being sent for a received ICE check, or indeed knowing when a response is being sent at all.

Send an ICE check

When the App requests an ICE check to be sent, the ICE agent follows the same process as performing an agent-initiated check, i.e. send a binding request over the candidate pair (RFC 8445 section 7.2.4) and change the candidate pair state to In-Progress.

If the requested candidate pair is already in In-Progress state, the method fails with a rejected promise.

If the interval since the previous check - over any candidate pair and whether initiated by the ICE agent or the App - is less than the Ta timer value (50ms, RFC 8445 appendix B.1), the method fails with a rejected promise. Here, Option 1 gives the App a clear indication of when the previous check was sent (i.e. when a Promise<RTCIceCheckRequest> resolved) while Option 2 only indicates this for App-initiated checks. With Option 2, this leaves a small margin for an unfortunate timing conflict when the method may fail without the App at fault.

User agents could implement a stronger safeguard by throwing an error synchronously if the method is called repeatedly within a window longer than the Ta timer value.

A typical App can let through all ICE checks in the beginning of a session until some viable candidate pairs are discovered, and then engage the prevent and send mechanisms to finely control ICE checks over the remained of the session.

Example usage

Option 1: Linked Promises
const pc = …;
const ice = pc.getTransceivers()[0].sender.transport.iceTransport;

ice.onicecandidatepaircheck = async(event) => {
   if (shouldNotCheck(event.candidatePair)) {
       event.preventDefault();    // prevent a check
       return;
   }
   const request = await event.request;
   handleCheck(request);
}

const request = await ice.checkCandidatePair(alternatePair);    // send a check
handleCheck(request);

function handleCheck(request) {
   try {
       const response = await request.response;
       const rtt = response.receivedTime - request.sentTime;
       // … do something with rtt …
       if (response.error) {
           // … do something with error …
       }
   } catch(error) {
       // … do something with timeout …
   }
}
Option 2: Flat events
const pc = …;
const ice = pc.getTransceivers()[0].sender.transport.iceTransport;

ice.onicecandidatepaircheck = (event) => {
   if (shouldNotCheck(event.candidatePair)) {
       event.preventDefault();     // prevent a check
   }
}

ice.onicecandidatepaircheckcomplete = handleCheck;

ice.checkCandidatePair(alternatePair);    // send a check

function handleCheck(event) {
   if (event.receivedTime) {
       const rtt = event.receivedTime - event.sentTime;
       // … do something with rtt …
   }
   else {
       // … do something with timeout …
   }
   if (event.error) {
       // … do something with error …
   }
}

cc: @pthatcherg

dontcallmedom-bot commented 3 months ago

This issue was discussed in WebRTC June 18 2024 meeting – 18 June 2024 (ICE improvements: send and prevent ICE check over a candidate pair)