Skip to content
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

Only works on localhost (see PR #23 for fix) #22

Open
yishengjiang99 opened this issue May 17, 2020 · 12 comments
Open

Only works on localhost (see PR #23 for fix) #22

yishengjiang99 opened this issue May 17, 2020 · 12 comments

Comments

@yishengjiang99
Copy link

has it been tested with a remote host

@lhcdims
Copy link

lhcdims commented May 18, 2020

I believe we need to use ssl certs as well as 'https' servers in the index.js in order to run the examples using a remote browser.

@yandeu
Copy link

yandeu commented May 18, 2020

I do not know if the examples work. But I have deployed an app on ec2 for testing, and it works without https.

@yishengjiang99
Copy link
Author

there's no icecandidates being exchanged after the two https calls, 'connect' and 'remote-description'...

https://dsp.grepawk.com/guest/datachannel-buffer-limits/index.html

@lhcdims
Copy link

lhcdims commented May 18, 2020

@yandeu, for video broadcasting in browser, I think we need to use https in order to get the access right to the camera and microphone.

@yishengjiang99
Copy link
Author

@yandeu i think u just need to post the ice candidates on a third call or something, like https://github.com/yishengjiang99/grepaudio/blob/master/postTracks.js

oh and use a real turn server. u can use the one im using

@lhcdims
Copy link

lhcdims commented May 18, 2020

When I use:

https://www.myurl.com:8851

to access the ping pong example (By using a remote browser), I got the following error:

$ npm start

> [email protected] start /home/lichiukenneth/nodejs/node-webrtc-examples
> node index.js

HTTPS Server running on port 8851
Error: Timed out waiting for host candidates
    at Timeout._onTimeout (/home/lichiukenneth/nodejs/node-webrtc-examples/lib/server/connections/webrtcconnection.js:163:21)
    at listOnTimeout (internal/timers.js:531:17)
    at processTimers (internal/timers.js:475:7)

I enclose 2 files, namely:

  1. node-webrtc-examples/index.js
'use strict';

  
// Certificate
const fs = require('fs');
const http = require('http');
const https = require('https');
const privateKey = fs.readFileSync('/home/lichiukenneth/Downloads/cert/privkey_thisapp.zephan.top_20200725.pem', 'utf8');
const certificate = fs.readFileSync('/home/lichiukenneth/Downloads/cert/cert_thisapp.zephan.top_20200725.pem', 'utf8');
const ca = fs.readFileSync('/home/lichiukenneth/Downloads/cert/chain_thisapp.zephan.top_20200725.pem', 'utf8');
const credentials = {
	key: privateKey,
	cert: certificate,
	ca: ca
};


const bodyParser = require('body-parser');
const browserify = require('browserify-middleware');
const express = require('express');
const { readdirSync, statSync } = require('fs');
const { join } = require('path');

const { mount } = require('./lib/server/rest/connectionsapi');
const WebRtcConnectionManager = require('./lib/server/connections/webrtcconnectionmanager');

const app = express();

app.use(bodyParser.json());

const examplesDirectory = join(__dirname, 'examples');

const examples = readdirSync(examplesDirectory).filter(path =>
  statSync(join(examplesDirectory, path)).isDirectory());

function setupExample(example) {
  const path = join(examplesDirectory, example);
  const clientPath = join(path, 'client.js');
  const serverPath = join(path, 'server.js');

  app.use(`/${example}/index.js`, browserify(clientPath));
  
  app.get(`/${example}/index.html`, (req, res) => {
    res.sendFile(join(__dirname, 'html', 'index.html'));
  });

  const options = require(serverPath);
  const connectionManager = WebRtcConnectionManager.create(options);
  mount(app, connectionManager, `/${example}`);

  return connectionManager;
}

// app.get('/', (req, res) => res.redirect(`${examples[9]}/index.html`));
app.get('/', (req, res) => res.redirect(`${examples[0]}/index.html`));


const connectionManagers = examples.reduce((connectionManagers, example) => {
  const connectionManager = setupExample(example);
  return connectionManagers.set(example, connectionManager);
}, new Map());


const httpServer = http.createServer(app);
const httpsServer = https.createServer(credentials, app);

/*
httpServer.listen(80, () => {
	console.log('HTTP Server running on port 80');
});
*/

httpsServer.listen(8851, () => {
	console.log('HTTPS Server running on port 8851');
});

 
  
/*
const server = app.listen(80, () => {
  const address = server.address();
  console.log(`http://localhost:${address.port}\n`);

  server.once('close', () => {
    connectionManagers.forEach(connectionManager => connectionManager.close());
  });
});
*/
  1. node-webrtc-examples/lib/server/connections/webrtcconnection.js (Both the google stun server and my own turn server give the same error)
'use strict';

const DefaultRTCPeerConnection = require('wrtc').RTCPeerConnection;

const Connection = require('./connection');

const TIME_TO_CONNECTED = 10000;
const TIME_TO_HOST_CANDIDATES = 3000;  // NOTE(mroberts): Too long.
const TIME_TO_RECONNECTED = 10000;

class WebRtcConnection extends Connection {
  constructor(id, options = {}) {
    super(id);

    options = {
      RTCPeerConnection: DefaultRTCPeerConnection,
      beforeOffer() {},
      clearTimeout,
      setTimeout,
      timeToConnected: TIME_TO_CONNECTED,
      timeToHostCandidates: TIME_TO_HOST_CANDIDATES,
      timeToReconnected: TIME_TO_RECONNECTED,
      ...options
    };

    const {
      RTCPeerConnection,
      beforeOffer,
      timeToConnected,
      timeToReconnected
    } = options;

//    const peerConnection = new RTCPeerConnection({
//      sdpSemantics: 'unified-plan'
//    });
//    const peerConnection = new RTCPeerConnection({ sdpSemantics: 'unified-plan', iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] });
    const peerConnection = new RTCPeerConnection({ sdpSemantics: 'unified-plan', iceServers: [{ urls: 'turn:my.turnserver.com:3307', username: 'user', credential: 'password' }] });

    beforeOffer(peerConnection);

    let connectionTimer = options.setTimeout(() => {
      if (peerConnection.iceConnectionState !== 'connected'
        && peerConnection.iceConnectionState !== 'completed') {
        this.close();
      }
    }, timeToConnected);

    let reconnectionTimer = null;

    const onIceConnectionStateChange = () => {
      if (peerConnection.iceConnectionState === 'connected'
        || peerConnection.iceConnectionState === 'completed') {
        if (connectionTimer) {
          options.clearTimeout(connectionTimer);
          connectionTimer = null;
        }
        options.clearTimeout(reconnectionTimer);
        reconnectionTimer = null;
      } else if (peerConnection.iceConnectionState === 'disconnected'
        || peerConnection.iceConnectionState === 'failed') {
        if (!connectionTimer && !reconnectionTimer) {
          const self = this;
          reconnectionTimer = options.setTimeout(() => {
            self.close();
          }, timeToReconnected);
        }
      }
    };

    peerConnection.addEventListener('iceconnectionstatechange', onIceConnectionStateChange);

    this.doOffer = async () => {
      const offer = await peerConnection.createOffer();
      await peerConnection.setLocalDescription(offer);
      try {
        await waitUntilIceGatheringStateComplete(peerConnection, options);
      } catch (error) {
        this.close();
        throw error;
      }
    };

    this.applyAnswer = async answer => {
      await peerConnection.setRemoteDescription(answer);
    };

    this.close = () => {
      peerConnection.removeEventListener('iceconnectionstatechange', onIceConnectionStateChange);
      if (connectionTimer) {
        options.clearTimeout(connectionTimer);
        connectionTimer = null;
      }
      if (reconnectionTimer) {
        options.clearTimeout(reconnectionTimer);
        reconnectionTimer = null;
      }
      peerConnection.close();
      super.close();
    };

    this.toJSON = () => {
      return {
        ...super.toJSON(),
        iceConnectionState: this.iceConnectionState,
        localDescription: this.localDescription,
        remoteDescription: this.remoteDescription,
        signalingState: this.signalingState
      };
    };

    Object.defineProperties(this, {
      iceConnectionState: {
        get() {
          return peerConnection.iceConnectionState;
        }
      },
      localDescription: {
        get() {
          return descriptionToJSON(peerConnection.localDescription, true);
        }
      },
      remoteDescription: {
        get() {
          return descriptionToJSON(peerConnection.remoteDescription);
        }
      },
      signalingState: {
        get() {
          return peerConnection.signalingState;
        }
      }
    });
  }
}

function descriptionToJSON(description, shouldDisableTrickleIce) {
  return !description ? {} : {
    type: description.type,
    sdp: shouldDisableTrickleIce ? disableTrickleIce(description.sdp) : description.sdp
  };
}

function disableTrickleIce(sdp) {
  return sdp.replace(/\r\na=ice-options:trickle/g, '');
}

async function waitUntilIceGatheringStateComplete(peerConnection, options) {
  if (peerConnection.iceGatheringState === 'complete') {
    return;
  }

  const { timeToHostCandidates } = options;

  const deferred = {};
  deferred.promise = new Promise((resolve, reject) => {
    deferred.resolve = resolve;
    deferred.reject = reject;
  });

  const timeout = options.setTimeout(() => {
    peerConnection.removeEventListener('icecandidate', onIceCandidate);
    deferred.reject(new Error('Timed out waiting for host candidates'));
  }, timeToHostCandidates);

  function onIceCandidate({ candidate }) {
    if (!candidate) {
      options.clearTimeout(timeout);
      peerConnection.removeEventListener('icecandidate', onIceCandidate);
      deferred.resolve();
    }
  }

  peerConnection.addEventListener('icecandidate', onIceCandidate);

  await deferred.promise;
}

module.exports = WebRtcConnection;

What's wrong with my setup? thanks.

@yandeu
Copy link

yandeu commented May 18, 2020

@yandeu, for video broadcasting in browser, I think we need to use https in order to get the access right to the camera and microphone.

Oh yes, I guess you're right. I only use dataChannels.

@yishengjiang99
Copy link
Author

yishengjiang99 commented May 18, 2020 via email

@yishengjiang99 yishengjiang99 changed the title Only works on localhost Only works on localhost (see PR #23 for fix) May 31, 2020
@yishengjiang99
Copy link
Author

@markandrus
Copy link
Member

@lhcdims

I believe we need to use ssl certs as well as 'https' servers in the index.js in order to run the examples using a remote browser.

It should also be sufficient to place a TLS-terminating reverse proxy in front of the Node.js server. That's how many Node.js-based HTTP servers are deployed. They don't terminate TLS themselves and instead rely on a reverse proxy (like NGINX).

@markandrus
Copy link
Member

@yishengjiang99

IceCandidate only happens after apply answer, in this case, the second remote connection call.. the waitForCandidate before timeout need to move there.

ICE candidate gathering starts as soon as setLocalDescription is called (see JSEP 3.5.1). In this project, the server applies its offer via setLocalDescription, which immediately begins gathering "host" candidates (as opposed to other candidate types like "srflx", "prflx" or "relay"). It works this way, because neither STUN nor TURN servers are provided. This is by design: the examples are intended to simulate an ice-lite server with a public IP address; such a server doesn't need STUN or TURN to discover its own ICE candidates, although its clients may still need a STUN or TURN server to discover their own candidates.

Now, I guess the problem you have is very similar to #2: it could be that the server is discovering some private IP that the client doesn't know about. While introducing STUN and TURN servers could be an option, I think a low-tech solution (just find/replace the private IP with the public IP in the server's offer) is preferable. I described this approach in my comment here.

@gbfarah
Copy link

gbfarah commented Nov 24, 2021

Hi Markandrus
I have few issues in this area that I would appreciate feedback

  1. The act of adding iceServers as below to webrtcconnection.js results in "Timed out waiting for host candidates'" error. Does node.js wrtc actually support passing your own iceServer ? .. I believe that is what lhcdims was hitting above. With the one line change below all examples actually fail
    const peerConnection = new RTCPeerConnection({
      sdpSemantics: 'unified-plan',
      iceServers: [
        {
          urls: ['stun:stun.l.google.com:19302']
        }
      ]
    });
  1. Can you elaborate on what changes need to be made to SDP to ensure that servers public IP address is always communicated . Is it correct to simply delete ice candidates and leave the public IP address in the "c=" field ?

  2. I trying to experiment with allowing two webrtconnection to talk to each other using the SDPs generated (with no Ice servers configured ) using code below

        //Two types of connection manager... One that generates connections with offer (isServer true) and one simulating
       // client connect whereby client answers an offer first
         this.serverConnectionManager = WebRtcConnectionManager.create({beforeOffer: beforeOffer , beforeAnswer : beforeAnswer, isServer : true});

         this.clientConnectionManager = WebRtcConnectionManager.create({beforeOffer: beforeOffer , beforeAnswer : beforeAnswer, isServer : false});

        const clientConnection = await this.clientConnectionManager.createConnectionWithoutOffer();
        const serverConnection = await this.serverConnectionManager.createConnection();
        await clientConnection.applyAnswer(serverConnection.localDescription);
        await clientConnection.createAnswer() ;
        await serverConnection.applyAnswer(clientConnection.localDescription);

Within WebRTCconnection.js
isServer is used to call beforeOffer if true and beforeAnswer if false

    this.createAnswer = async () => {
      if(!isServer)
        beforeAnswer(peerConnection) ; 
      const answer = await peerConnection.createAnswer();

      await peerConnection.setLocalDescription(answer);

      
      try {
        await waitUntilIceGatheringStateComplete(peerConnection, options);
      } catch (error) {
        this.close();
        throw error;
      }
      
    };

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

No branches or pull requests

5 participants