node-webrtc / node-webrtc-examples

MediaStream and RTCDataChannel examples using node-webrtc
508 stars 161 forks source link

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

Open yishengjiang99 opened 4 years ago

yishengjiang99 commented 4 years ago

has it been tested with a remote host

lhcdims commented 4 years ago

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 commented 4 years ago

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

yishengjiang99 commented 4 years ago

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 commented 4 years ago

@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 commented 4 years ago

@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 commented 4 years ago

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

> node-webrtc-examples@0.1.0 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()); }); }); */


2. 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 commented 4 years ago

@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 commented 4 years ago

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

Sent from my iPhone

On May 18, 2020, at 6:55 AM, Ken notifications@github.com wrote:

 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

node-webrtc-examples@0.1.0 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:

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()); }); }); */ 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.

— You are receiving this because you authored the thread. Reply to this email directly, view it on GitHub, or unsubscribe.

yishengjiang99 commented 4 years ago

https://github.com/node-webrtc/node-webrtc-examples/pulls

markandrus commented 4 years ago

@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 commented 4 years ago

@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 commented 2 years ago

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;
      }

    };