使用 Express 和“websocket”包的 WebRTC 信令服务器

Posted

技术标签:

【中文标题】使用 Express 和“websocket”包的 WebRTC 信令服务器【英文标题】:WebRTC signaling server using Express & 'websocket' packages 【发布时间】:2016-04-08 16:06:32 【问题描述】:

我正在使用 Rob Manson 的 WebRTC 入门 中的以下代码。它是使用 Node.js 实现的。该代码启动了一个实时视频通话,我让它在 Web 浏览器的 2 个选项卡之间按预期运行。我只是想修改它,以便它使用 Express 而不是“http”包。

我遇到的问题是我的版本中没有显示视频。 “呼叫者”按预期工作,但随后“被呼叫者”停在“请稍候...连接您的呼叫...”消息。在浏览器控制台或我的终端中没有检测到错误,并且花了一天时间试图解决这个问题,我仍然不知道我哪里出错了。

这是原始的信令服务器文件:

// useful libs
var http = require("http");
var fs = require("fs");
var websocket = require("websocket").server;

// general variables
var port = 8000;
var webrtc_clients = [];
var webrtc_discussions = ;

// web server functions
var http_server = http.createServer(function(request, response) 
      var matches = undefined;
      if (matches = request.url.match("^/images/(.*)")) 
            var path = process.cwd()+"/images/"+matches[1];

            fs.readFile(path, function(error, data) 
              if (error) 
                      log_error(error);
               else 
                      response.end(data);
              
            );
       else 
          response.end(page);
      
);

http_server.listen(port, function() 
        log_comment("server listening (port "+port+")");
);

var page = undefined;
fs.readFile("basic_video_call.html", function(error, data) 
      if (error) 
          log_error(error);
       else 
          page = data;
      
);

// web socket functions
var websocket_server = new websocket(
  httpServer: http_server
);
websocket_server.on("request", function(request) 

  log_comment("new request ("+request.origin+")");

  var connection = request.accept(null, request.origin);
  log_comment("new connection ("+connection.remoteAddress+")");

  webrtc_clients.push(connection);
  connection.id = webrtc_clients.length-1;

  connection.on("message", function(message) 
    if (message.type === "utf8") 
      log_comment("got message "+message.utf8Data);

          var signal = undefined;
          try  signal = JSON.parse(message.utf8Data);  catch(e)  ;

          if (signal) 
            if (signal.type === "join" && signal.token !== undefined) 
                  try 
                        if (webrtc_discussions[signal.token] === undefined) 
                                webrtc_discussions[signal.token] = ;
                        
                   catch(e)  ;
                  try 
                      webrtc_discussions[signal.token][connection.id] = true;
                   catch(e)  ;
             else if (signal.token !== undefined) 
                  try 
                        Object.keys(webrtc_discussions[signal.token]).forEach(function(id) 
                              if (id != connection.id) 
                                      webrtc_clients[id].send(message.utf8Data, log_error);
                              
                        );
                   catch(e)  ;
             else 
                log_comment("invalid signal: "+message.utf8Data);
            
           else 
                  log_comment("invalid signal: "+message.utf8Data);
          
    
  );

  connection.on("close", function(connection) 
        log_comment("connection closed ("+connection.remoteAddress+")"); 

        Object.keys(webrtc_discussions).forEach(function(token) 
              Object.keys(webrtc_discussions[token]).forEach(function(id) 
                if (id === connection.id) 
                    delete webrtc_discussions[token][id];
                
              );
        );
  );
);

// utility functions
function log_error(error) 
  if (error !== "Connection closed" && error !== undefined) 
    log_comment("ERROR: "+error);
  

function log_comment(comment) 
  console.log((new Date())+" "+comment);

这是我修改后的文件。仅更改了第一位:

// useful libs
var http = require("http");
var fs = require("fs");
var websocket = require("websocket").server;
var express = require('express');
var morgan = require('morgan');
var bodyParser = require('body-parser');

// general variables
var hostname = 'localhost';
var port = 8000;
var webrtc_clients = [];
var webrtc_discussions = ;

var expressApp = express();

expressApp.use(morgan('dev'));

var myRouter = express.Router();

myRouter.use(bodyParser.json());

// web server functions
myRouter.route('/').all(function(request,response,next) 
  var matches = undefined;
  if (matches = request.url.match("^/images/(.*)")) 
    var path = process.cwd() +"/images/"+matches[1];

    debugger;
    console.log("PATH: " + path);

    fs.readFile(path, function(error, data) 
      if (error) 
        log_error(error);
       else 
        response.end(data);
      
    );
   else 
    response.end(page);
  
);
//////////////////////


expressApp.use('/',myRouter);

expressApp.listen(port, hostname, function()
  console.log(`Server running at http://$hostname:$port/`);
);

/////////////////////// **I CHANGED NOTHING BELOW HERE** ////////////

var page = undefined;
fs.readFile("basic_video_call.html", function(error, data) 
  if (error) 
    log_error(error);
   else 
    page = data;
  
);



// web socket functions
var websocket_server = new websocket(
  httpServer: expressApp
);

websocket_server.on("request", function(request) 


  log_comment("new request ("+request.origin+")");

  var connection = request.accept(null, request.origin);
  log_comment("new connection ("+connection.remoteAddress+")");

  webrtc_clients.push(connection);
  connection.id = webrtc_clients.length-1;

  connection.on("message", function(message) 
    if (message.type === "utf8") 
      log_comment("got message "+message.utf8Data);

      var signal = undefined;
      try  signal = JSON.parse(message.utf8Data);  catch(e)  ;
      if (signal) 
        if (signal.type === "join" && signal.token !== undefined) 
          try 
            if (webrtc_discussions[signal.token] === undefined) 
              webrtc_discussions[signal.token] = ;
            
           catch(e)  ;
          try 
            webrtc_discussions[signal.token][connection.id] = true;
           catch(e)  ;
         else if (signal.token !== undefined) 
          try 
            Object.keys(webrtc_discussions[signal.token]).forEach(function(id) 
              if (id != connection.id) 
                webrtc_clients[id].send(message.utf8Data, log_error);
              
            );
           catch(e)  ;
         else 
          log_comment("invalid signal: "+message.utf8Data);
        
       else 
        log_comment("invalid signal: "+message.utf8Data);
      
    
  );

  connection.on("close", function(connection) 
    log_comment("connection closed ("+connection.remoteAddress+")");    
    Object.keys(webrtc_discussions).forEach(function(token) 
      Object.keys(webrtc_discussions[token]).forEach(function(id) 
        if (id === connection.id) 
          delete webrtc_discussions[token][id];
        
      );
    );
  );
);

// utility functions
function log_error(error) 
  if (error !== "Connection closed" && error !== undefined) 
    log_comment("ERROR: "+error);
  

function log_comment(comment) 
  console.log((new Date())+" "+comment);

另外,这里是处理 WebRTC 调用的代码,我没有更改:

<!DOCTYPE html>
<html>
<head>
<script>
/*

  webrtc_polyfill.js by Rob Manson
  NOTE: Based on adapter.js by Adam Barth

  The MIT License

  Copyright (c) 2010-2013 Rob Manson, http://buildAR.com. All rights reserved.

  Permission is hereby granted, free of charge, to any person obtaining a copy
  of this software and associated documentation files (the "Software"), to deal
  in the Software without restriction, including without limitation the rights
  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  copies of the Software, and to permit persons to whom the Software is
  furnished to do so, subject to the following conditions:

  The above copyright notice and this permission notice shall be included in
  all copies or substantial portions of the Software.

  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  THE SOFTWARE.

*/

var webrtc_capable = true;
var rtc_peer_connection = null;
var rtc_session_description = null;
var get_user_media = null;
var connect_stream_to_src = null;
var stun_server = "stun.l.google.com:19302";

if (navigator.getUserMedia)  // WebRTC 1.0 standard compliant browser
  rtc_peer_connection = RTCPeerConnection;
  rtc_session_description = RTCSessionDescription;
  get_user_media = navigator.getUserMedia.bind(navigator);
  connect_stream_to_src = function(media_stream, media_element) 
    // https://www.w3.org/Bugs/Public/show_bug.cgi?id=21606
    media_element.srcObject = media_stream;
    media_element.play();
  ;
 else if (navigator.mediaDevices.getUserMedia)  // early firefox webrtc implementation
  rtc_peer_connection = mozRTCPeerConnection;
  rtc_session_description = mozRTCSessionDescription;
  get_user_media = navigator.mozGetUserMedia.bind(navigator);
  connect_stream_to_src = function(media_stream, media_element) 
    media_element.mozSrcObject = media_stream;
    media_element.play();
  ;
  stun_server = "74.125.31.127:19302";
 else if (navigator.webkitGetUserMedia)  // early webkit webrtc implementation
  rtc_peer_connection = webkitRTCPeerConnection;
  rtc_session_description = RTCSessionDescription;
  get_user_media = navigator.webkitGetUserMedia.bind(navigator);
  connect_stream_to_src = function(media_stream, media_element) 
    media_element.src = webkitURL.createObjectURL(media_stream);
  ;
 else 
  alert("This browser does not support WebRTC - visit WebRTC.org for more info");
  webrtc_capable = false;

</script>
<script>
/*

  basic_video_call.js by Rob Manson

  The MIT License

  Copyright (c) 2010-2013 Rob Manson, http://buildAR.com. All rights reserved.

  Permission is hereby granted, free of charge, to any person obtaining a copy
  of this software and associated documentation files (the "Software"), to deal
  in the Software without restriction, including without limitation the rights
  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  copies of the Software, and to permit persons to whom the Software is
  furnished to do so, subject to the following conditions:

  The above copyright notice and this permission notice shall be included in
  all copies or substantial portions of the Software.

  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  THE SOFTWARE.

*/

var call_token; // unique token for this call
var signaling_server; // signaling server for this call
var peer_connection; // peer connection object

function start() 
  // create the WebRTC peer connection object
  peer_connection = new rtc_peer_connection( // RTCPeerConnection configuration 
    "iceServers": [ // information about ice servers
       "url": "stun:"+stun_server , // stun server info
    ]
  );

  // generic handler that sends any ice candidates to the other peer
  peer_connection.onicecandidate = function (ice_event) 
    if (ice_event.candidate) 
      signaling_server.send(
        JSON.stringify(
          token:call_token,
          type: "new_ice_candidate",
          candidate: ice_event.candidate ,
        )
      );
    
  ;

  // display remote video streams when they arrive using local <video> MediaElement
  peer_connection.onaddstream = function (event) 
    connect_stream_to_src(event.stream, document.getElementById("remote_video"));
    // hide placeholder and show remote video
    document.getElementById("loading_state").style.display = "none";
    document.getElementById("open_call_state").style.display = "block";
  ;

  // setup stream from the local camera 
  setup_video();

  // setup generic connection to the signaling server using the WebSocket API
  signaling_server = new WebSocket("ws://localhost:8000");

  if (document.location.hash === "" || document.location.hash === undefined)  // you are the Caller

    // create the unique token for this call 
    var token = Math.round(Math.random()*100);
    call_token = "#"+token;

    // set location.hash to the unique token for this call
    document.location.hash = token;

    signaling_server.onopen = function() 
      // setup caller signal handler
      signaling_server.onmessage = caller_signal_handler;

      // tell the signaling server you have joined the call 
      signaling_server.send(
        JSON.stringify( 
          token:call_token,
          type:"join",
        )
      );
    

    document.title = "You are the Caller";
    document.getElementById("loading_state").innerHTML = "Ready for a call...ask your friend to visit:<br/><br/>"+document.location;

   else  // you have a hash fragment so you must be the Callee 

    // get the unique token for this call from location.hash
    call_token = document.location.hash;

    signaling_server.onopen = function() 
      // setup caller signal handler
      signaling_server.onmessage = callee_signal_handler;

      // tell the signaling server you have joined the call 
      signaling_server.send(
        JSON.stringify( 
          token:call_token,
          type:"join",
        )
      );

      // let the caller know you have arrived so they can start the call
      signaling_server.send(
        JSON.stringify( 
          token:call_token,
          type:"callee_arrived",
        )
      );
    

    document.title = "You are the Callee";
    document.getElementById("loading_state").innerHTML = "One moment please...connecting your call...";
  



/* functions used above are defined below */

// handler to process new descriptions
function new_description_created(description) 
  peer_connection.setLocalDescription(
    description, 
    function () 
      signaling_server.send(
        JSON.stringify(
          token:call_token,
          type:"new_description",
          sdp:description 
        )
      );
    , 
    log_error
  );


// handle signals as a caller
function caller_signal_handler(event) 
  var signal = JSON.parse(event.data);
  if (signal.type === "callee_arrived") 
    peer_connection.createOffer(
      new_description_created, 
      log_error
    );
   else if (signal.type === "new_ice_candidate") 
    peer_connection.addIceCandidate(
      new RTCIceCandidate(signal.candidate)
    );
   else if (signal.type === "new_description") 
    peer_connection.setRemoteDescription(
      new rtc_session_description(signal.sdp), 
      function () 
        if (peer_connection.remoteDescription.type == "answer") 
          // extend with your own custom answer handling here
        
      ,
      log_error
    );
   else 
    // extend with your own signal types here
  


// handle signals as a callee
function callee_signal_handler(event) 
  var signal = JSON.parse(event.data);
  if (signal.type === "new_ice_candidate") 
    peer_connection.addIceCandidate(
      new RTCIceCandidate(signal.candidate)
    );
   else if (signal.type === "new_description") 
    peer_connection.setRemoteDescription(
      new rtc_session_description(signal.sdp), 
      function () 
        if (peer_connection.remoteDescription.type == "offer") 
          peer_connection.createAnswer(new_description_created, log_error);
        
      ,
      log_error
    );
   else 
    // extend with your own signal types here
  


// setup stream from the local camera 
function setup_video() 
  get_user_media(
     
      "audio": true, // request access to local microphone
      "video": true  // request access to local camera
      //"video": mandatory: minHeight:8, maxHeight:8, minWidth:8, maxWidth:8
    , 
    function (local_stream)  // success callback
      // display preview from the local camera & microphone using local <video> MediaElement
      connect_stream_to_src(local_stream, document.getElementById("local_video"));
      // add local camera stream to peer_connection ready to be sent to the remote peer
      peer_connection.addStream(local_stream);
    ,
    log_error
  );


// generic error handler
function log_error(error) 
  console.log(error);


</script>
<style>
html, body 
  padding: 0px;
  margin: 0px;
  font-family: "Arial","Helvetica",sans-serif;

#loading_state 
  position: absolute;
  top: 45%;
  left: 0px;
  width: 100%;
  font-size: 20px;
  text-align: center;

#open_call_state 
  display: none;

#local_video 
  position: absolute;
  top: 10px;
  left: 10px;
  width: 160px;
  height: 120px;
  background: #333333;

#remote_video 
  position: absolute;
  top: 0px;
  left: 0px;
  width: 1024px;
  height: 768px;
  background: #999999;

</style>
</head>
<body onload="start()">
    <div id="loading_state">
        loading...
    </div>

    <div id="open_call_state">
        <video id="remote_video"></video>
        <video id="local_video"></video>
    </div>
</body>
</html>

我也对不使用 Express 但仍支持 WebRTC 身份验证的解决方案持开放态度。非常感谢您的帮助。

【问题讨论】:

【参考方案1】:

好吧,我找到了解决方案。它只涉及替换这一行:

expressApp.listen(port, hostname, function() 
  console.log(`Server running at http://$hostname:$port/`);

有了这个:

var webServer = http.createServer(expressApp).listen(8000);

this thread 解释了这个工作的原因。为一个小问题浪费任何人的时间表示歉意。

【讨论】:

以上是关于使用 Express 和“websocket”包的 WebRTC 信令服务器的主要内容,如果未能解决你的问题,请参考以下文章

带有 socket.io 和 express 的 Websocket

GraphQL 订阅、websocket、nodejs、Express 会话

express-ws 中间件无法正确创建 websocket 服务器

使用 Apollo Express、Nginx 和 docker-compose 保护 websocket

如何在 Express 应用程序中通过 SSL 获取 websocket?

为 Express 定义 Websocket 路由