Skip to content

Example of configuring iceServers to specify a turn server #14

Description

@vincentfretin

I have an elixir backend where I can retrieve the ice servers from a turn server provider or a self hosted coturn, it's a graphql query that return something like this:

           {
              "iceServers": {
                "credential": "TheCredential",
                "urls": [
                  "stun:{turn_host}",
                  "turn:{turn_host}:80?transport=udp",
                  "turn:{turn_host}:3478?transport=udp",
                  "turn:{turn_host}:80?transport=tcp",
                  "turn:{turn_host}:3478?transport=tcp",
                  "turns:{turn_host}:443?transport=tcp",
                  "turns:{turn_host}:5349?transport=tcp"
                ],
                "username": "TheUserName"
              }
            }
          }

I have a global setupPeerConnectionConfig function.

const setupPeerConnectionConfig = (adapter, iceServers) => {
  const qs = new URLSearchParams(location.search);
  const forceTurn = qs.get("force_turn");
  const forceTcp = qs.get("force_tcp");
  const peerConnectionConfig = {};

  if (iceServers && iceServers.urls !== null) {
    if (forceTcp) {
      iceServers.urls = iceServers.urls.filter((url) => url.startsWith("turn") && url.endsWith("tcp"));
    }
    const hasStunServer = !!iceServers.urls.find((url) => url.startsWith('stun'));
    const newIceServers = [];
    if (!forceTcp && !forceTurn && !hasStunServer) {
      newIceServers.push({ urls: "stun:stun1.l.google.com:19302" });
    }
    // remove __typename from the graphql query
    newIceServers.push({username: iceServers.username, credential: iceServers.credential, urls: iceServers.urls});

    peerConnectionConfig.iceServers = newIceServers;

    if (forceTurn || forceTcp) {
      peerConnectionConfig.iceTransportPolicy = "relay";
    }
  } else {
    peerConnectionConfig.iceServers = [
      { urls: "stun:stun1.l.google.com:19302" },
      { urls: "stun:stun2.l.google.com:19302" }
    ];
  }

  adapter.setPeerConnectionConfig(peerConnectionConfig);
};

To test that your config is right, you can force going through turn server (iceTransportPolicy = "relay")
by adding in the url ?force_turn=true. To force turn via tcp, use ?force_tcp=true. Look at about:webrtc in Firefox to see the selected candidate.

In your adapter-ready listener, call the setupPeerConnectionConfig with the retrieved iceServers.

const iceServers = await callToServerToGetIceServers();

scene.addEventListener("adapter-ready", ({ detail: adapter }) => {
  // don't use any await here, it won't be awaited before connecting
  setupPeerConnectionConfig(adapter, iceServers);
  // ... do your other things
});

scene.emit("connect");

Getting an iceServers config asynchronously and using connectOnLoad: true are incompatible, there is no await for adapter-ready listener in https://github.com/networked-aframe/networked-aframe/blob/41a6b484c8208ecb24e85855542a3fa40faf7bec/src/components/networked-scene.js#L33 so the user will connect before we get the config.

If your turn credential is temporary, you will need to call regularly

  const iceServers = await callToServerToGetIceServers();
  setupPeerConnectionConfig(NAF.connection.adapter, iceServers);

otherwise new peer connections will use expired turn credential and the connection won't succeed. You can use a setTimout similar to the JWT I explained here mozilla/janus-plugin-sfu#77 (comment)

I should probably push both the linked comment and this issue into a document that I push in this repo.

Metadata

Metadata

Assignees

No one assigned

    Labels

    documentationImprovements or additions to documentation

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions