licode安装过程及发布局域网服务

本文详细介绍如何使用WebRTC和Licode搭建视频会议系统,包括系统环境配置、前后端分离部署、nginx反向代理集成、TURN服务器安装配置,以及前端集成和WebSocket服务器代码示例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

  • 系统说明:

本系统是基于谷歌的webRTC浏览器端视频点对点技术实现网页视频及语音即时通讯。采用开源项目licode作为聊天室管理,以及其web端js模块。

    目前系统采用前后端分离以及多服务器方式部署,采用nginx作为反向代理集成licode、apiserver、front。

    环境要求:

操作系统:ubuntu14、nginx、nodejs、RibbitMQ

  • Licode安装

在ubuntu中安装:

先从GitHub上下载源码

  1. git clone https://github.com/ging/licode.git
  2. cd licode

解决HTTPS/SSL访问错误:

由于是配置文件名不统一导致,而且在Sokect.js中的token.secure参数无法配置,故直接修改。

/licode/erizo_controller/erizoClient/src/Socket.js

167ccb46d10983a9ec6b8e12c3e50c6588d.jpg

licode_default.js文件修改SSL相关配置,注意ssl证书也需要修改

ssl相关设置为true;

然后复制licode_default.jslicode_config.js 

 

获得源码后进入licode/scripts/目录,复制licode_default.js为licode_config.js,同时将licode_default.js和licode_config.js都修改为如下配置:

// 配置turnserver服务器

config.erizoController.iceServers = [{'url': 'stun:stun.l.google.com:19302'},{'url': 'stun:服务器IP:3478'}];//注意,配置的服务器必须是可访问的,否则启动失败

// 开启 SSL

config.erizoController.ssl = true;

config.erizoController.listen_ssl = true; //default value: false

config.erizoController.listen_port = 8080; //default value: 8080

 

// 配置SSL 文件

config.erizoController.ssl_key = '/full/path/to/ssl.key';

config.erizoController.ssl_cert = '/full/path/to/ssl.crt';

 

 

依次序执行安装:

1、安装系统依赖

  1. ./scripts/installUbuntuDeps.sh

 

2、安装Erizo和Nuve

  1. ./scripts/installErizo.sh
  2. ./scripts/installNuve.sh

3、安装基本实例程序

  1. ./scripts/installBasicExample.sh

4、运行licode

  1. ./licode/scripts/initLicode.sh

5、运行基础实例web服务

  1. ./scripts/initBasicExample.sh

 

  • Licode API

官方地址:

http://licode.readthedocs.io/en/master/client_api/

包含 客户端API 、服务端API

具体详见官网。

 

 

 

  • turnserver安装

在使用WebRTC进行即时通讯时,需要使浏览器进行P2P通讯,但是由于NAT环境的复杂性,并不是所有情况下都能进行P2P,这时需要TURN Server来帮助客户端之间转发数据。rfc5766-turn-server是一个高性能的开源TURN Server实现。

 

以下是在EC2上使用Ubuntu操作系统安装rfc5766-turn-server

1. 下载安装包:

$ wget http://ftp.cn.debian.org/debian/pool/main/r/rfc5766-turn-server/rfc5766-turn-server_3.2.4.4-1_amd64.deb

2. 安装:

$ sudo apt-get update

$ sudo apt-get install gdebi-core

$ sudo gdebi rfc5766-turn-server_3.2.4.4-1_amd64.deb

安装完后,在/usr/share/doc/rfc5766-turn-server下有很多文档可参考。

3. 配置:

$ sudo vi /etc/turnserver.conf

---------------------------------------

// 配置IPEC2下需要配置listening-ipexternal-ip

listening-ip=172.31.4.37 注意:如果是未知IP,可设置0.0.0.0

external-ip=54.223.149.60

// TURN Server用于WebRTC时,必须使用long-term credential mechanism

lt-cred-mech

// 增加一个用户

user=username1:password1

// 设定realm

realm=mycompany.org

---------------------------------------

 

4. 启动:

sudo turnserver -c /etc/turnserver.conf --daemon

 

5. 服务启动后,在上一个WebRTC示例中更改iceServers后测试:

"iceServers": [{

    "url": "stun:stun.l.google.com:19302"

}, {

    "url": "turn:54.223.149.60",

    "username": "username1",

    "credential": "password1"

}]

 

更多安装信息在:http://turnserver.open-sys.org/downloads/v3.2.4.4/INSTALL

 

rfc5766-turn-server当然也有STUN Server的能力,但是需要给它配置2IP,以帮助探测客户端所在NAT环境的行为,这里没有做。

 

 

  • 前端集成

时序图

WebRTC相关时序:

4d5cefe6edd278cd88d2e29ead38a363b22.jpg

    浏览器通过candidate 来进行连接。连接之后,就无需与服务器进行连接。

在获得candidate时需要连接turnserver。具体是由浏览器webRTC模块执行。

 

Licode集成时序图:

b2bf591839b070f04494726c3e46ba323ad.jpg

 

 

Nginx反向代理配置

运行./scripts/initLicode.sh 和./scripts/initBasicExample.sh

Licode服务器提供

http端口 3000 3001 websocket 端口8080

https 端口 3004 websocket 端口 8080

nginx配置如下:

前端程序nginx配置:

server {

        listen       443 ssl;

        ssl_certificate      /usr/local/etc/nginx/server.crt;

        ssl_certificate_key  /usr/local/etc/nginx/server.key;

 

    #    ssl_session_cache    shared:SSL:1m;

        ssl_session_timeout  5m;

 

    #    ssl_ciphers  HIGH:!aNULL:!MD5;

    #    ssl_prefer_server_ciphers  on;

        server_name '192.168.10.225';

        if ($scheme = http) {

            return 301 https://$server_name$request_uri;

        }

        location / {

            root   html; #前端静态运行目录

            index  index.html index.htm;

            proxy_redirect off;

            proxy_set_header Host $host;

            proxy_set_header X-Real-IP $remote_addr;

            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

            proxy_pass http://127.0.0.1:8085/;

        }

    location /api{ #接口服务

        proxy_redirect off;

            proxy_set_header Host $host;

            proxy_set_header X-Real-IP $remote_addr;

            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

            proxy_pass http://127.0.0.1:8080/api;

    }

       #文件上传后存放目录

        location /upload{

            proxy_redirect off;

            proxy_set_header Host $host;

            proxy_set_header X-Real-IP $remote_addr;

            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

            proxy_pass http://127.0.0.1:8080/upload;

        }

#licode 创建token路径转发

        location /createToken{

            proxy_redirect off;

            proxy_set_header Host $host;

            proxy_set_header X-Real-IP $remote_addr;

            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

            proxy_pass http://192.168.10.228:3001/createToken;

        }

    }

 

 

 

 

代码示例

Websock服务器

io.on('connect', function (socket) {

    socket.on('message', function (data) {

        var data = JSON.parse(data);

        console.log("请求数据", data);

        //当前登录用户名

        var userId = data.from.id;

        //当前用户的聊天室id

        var roomId = data.roomId;

        socket.userId = userId;

        socket.roomId = roomId;

  

        function join(f) {

            //当用户与聊天室id存在时

            if (allUsers[roomId] == undefined) {

                allUsers[roomId] = {};

            }

            if (allSockets[roomId] == undefined) {

                allSockets[roomId] = {};

            }

            //保存用户信息

            allUsers[roomId][userId] = data.from;

            if (allSockets[roomId][userId] != undefined && allSockets[roomId][userId].scoket != undefined && f == true) {//给之前的连接发送退出信息

                sendTo(allSockets[roomId][userId].scoket,{

                    event: "leave"

                });

            }

            allSockets[roomId][userId] = {'scoket': socket};

        }

  

        try {

            switch (data.event) {

                case "heartbeat":

                    join(false);

                    break;

                //当有新用户加入时

                case "join":

                    join(true);

                    showUserInfo(allUsers[roomId]);

                    sendTo(socket, {

                        event: "join",

                        "message": "成功加入聊天室",

                        "success": true

                    });

                    break;

                //======== 即时通讯 IM  =========

                case "leave":

                    delete allUsers[roomId][userId];

                    showUserInfo(allUsers[roomId]);

                    break;

                case "msg"://IM消息

                    join();

                    if (data.message == undefined || data.message == '' || data.message == null) {

                        break;

                    }

                    //获取room用户

                    var roomId = data.roomId;

                    var ulist = allUsers[roomId];

                    data.datetime = new Date();

                    if (ulist != undefined) {

                        //查找用户连接发送信息

                        for (var u in ulist) {

                            if (ulist[u])

                                sendTo(allSockets[roomId][u].scoket, data);

                        }

                    }

                    break;

                case "historyMsg"://聊天历史

                    join();

                    //获取room用户

                    var roomId = data.roomId;

                    var ulist = allUsers[roomId];

                    break

                //======== 基于allWebsockets start =========

                case "sigin"://登录系统

                    for (var ws in allWebsockets) {

                        if (allWebsockets[ws])

                            sendTo(allWebsockets[ws], {

                                event: "sigin",

                                "message": "登录",

                                "success": true,

                                from: data.from

                            });

                    }

                    socket.mainId=data.from.id;

                    socket.userId=data.from.id;

                    allWebsockets[data.from.id] = socket;

                    break;

                case "joinChatRoom"://要求加入视频会议

                    var userList = data.userList;

                    if (userList) {

                        for (var i = 0; i < userList.length; i++) {

                            if (allWebsockets[userList[i]])

                                sendTo(allWebsockets[userList[i]], {

                                    event: "joinChatRoom",

                                    "message": data.message || "加入会议",

                                    from: data.from,

                                    roomId: data.roomId

                                });

                        }

                    }

                    allWebsockets[data.from.id] = socket;

                    break;

                case "rejectJoinChatRoom"://拒绝加入视频会议

                    sendTo(allWebsockets[data.to], {

                        event: "rejectJoinChatRoom",

                        "message": data.message || "加入会议",

                        from: data.from,

                        roomId: data.roomId

                    });

                    break;

                //=================================

            }

        }

        catch (ex) {

            console.log("异常:\t", ex)

        }

    });

  

    socket.on("disconnect", function () {

        if (true) {

            return;

        }

        var userId_ = socket.userId;

        var roomId_ = socket.roomId;

        if (userId_ != undefined && userId_ != null && userId_ != '') {

            if (roomId_||allUsers[roomId_] == undefined || allSockets[roomId_] == undefined) {

                return;

            }

            var ulist = allUsers[roomId_];

            if (ulist != undefined) {

                //查找用户连接发送信息

                for (var u in ulist) {

                    if (allSockets[roomId_][u])

                        sendTo(allSockets[roomId_][u].scoket, {event: 'leave', userId: userId_});

                }

            }

            //删除已经退出的用户

            delete allUsers[roomId_][userId_];

            if(socket.mainId)

                delete allWebsockets[socket.mainId];

            if (allUsers[roomId_] != undefined) {

                delete allSockets[roomId_][userId_];

                showUserInfo(allUsers[roomId_]);

            }

        }

    });

});

  

  function showUserInfo(allUsers) {

    console.log("在线用户:", allUsers);

    sendTo(io, {

        event: "showUserList",

        "userList": allUsers,

    });

}

  

  function removeUserInfo(roomId, userId) {

    delete allUsers[roomId][userId];

    delete allSockets[roomId][userId];

}

  

  function sendTo(connection, message) {

    console.log("send data:\t", message);

    if (connection != undefined)

        connection.send(message);

}

 

    启动websoket服务器 :

node server.js

以上包含两种websoket处理,如文本聊天处理、视频会议邀请处理。

 

前端集成

在/front/src/views/Main.vue中定义一个主要的websoket连接,来处理视频会议邀请。

需要在index.html引用socket.io.js

代码如下:

export default {
data() {

    return {
currentUser: this.$store.getters.getCurrentLoginInfo.user,
main_websocket:io.connect(Constants.wsUrl)

    };
},

  methods: {
send(message) {

  message.from = {id: this.currentUser.id, name: this.currentUser.name, avator: this.currentUser.avator};

  this.main_websocket.send(JSON.stringify(message));

}

},
created() {
//监听websoket消息

      this.main_websocket.on('message', (data) => {

        console.error("on message", data);

        switch (data.event) {

          case "joinChatRoom":

            var noticeName='roomId'+data.roomId;

            this.$Notice.open({title:'视频会议邀请',name:noticeName,render:(r)=>{

                return r('div',[r('div',data.from.name+'邀请您加入视频会议,是否接受?'),

                  r('Button',{

                    on: {

                      click:()=>{

                        this.$router.push("/chat/" + data.roomId);

                        this.$Notice.close(noticeName);

                      }

                    },props:{type:'primary',size:'small'}},'接受'),

                  r('Button',{

                    on: {click:()=>{

              this.send({event:"rejectJoinChatRoom",to:data.from.id,roomId:data.roomId});

                        this.$Notice.close(noticeName);

                      }

                    },props:{type:'error',size:'small'}},'拒绝')

                ]);

              },duration:10,onClose:()=>{

              }});

            break;

          case "sigin":

            break;

          case "rejectJoinChatRoom":

            this.$Notice.warning({title:'拒绝视频会议',desc:data.from.name+'拒绝加入视频会议。',duration:10});

            break;

          case "message":

            this.$Notice.info({title:data.title|'消息',desc:data.message,duration:10});

            break;

        }

      });

      this.send({event:"sigin"});

    }

};

视频会议邀约:

对应vuejs文件:/front/src/views/chat/index.vue

步骤代码如下:

import http from '../../api/http'

  import Room from './licodeRoom.vue'

  

  export default {

  data() {

    return {

      roomShow: false,

      hospno: null,

      patient:null,

      roomId: null,

      doctorList: [],

      tableHeight: window.innerHeight - 80

    }

  },

  methods: {

    chosePatient() {

      http.post(this,'/pt/patient/choseOne',{hospno:this.hospno},(resp)=>{

        var ret=resp.body;

        if(ret.code=='111'){

          this.patient=ret.value;

        }else{

          this.$Message.error(ret.msg);

        }

      });

    },

    joinRoom() {

      this.roomId = this.hospno;

      this.$router.push("/chat/"+this.roomId);

  this.$parent.send({event:'joinChatRoom',roomId:this.roomId,userList:this.doctorList});

    },

    closeDiv() {

      this.roomShow = false;

      this.hospno = null;

      this.roomId = null; 

    },

    loadRouter(){

      var params=this.$route.params;

      console.error(params);

      if(params.roomId!=''&&params.roomId!='-'&&params.roomId!='null'&&params.roomId!='undefined'){

        this.roomId=params.roomId;

        this.roomShow = true;

      }

    }

  },

  components: {

    Room

  },

  watch:{

    '$route'(val){ #监听路由变化'

      if(val&&val!='-'){ #如果路由参数存在且不为‘- 则加载路由,并弹出licodeRoom.vue的界面 加入视频会议

        this.loadRouter();

      }

    }

  },

  mounted() {

    this.loadRouter();

  },

  created() {

  }

}

 

 

licodeRoom.vue代码:

代码文件:/front/src/views/chat/licodeRoom.vue

//设置视频位置

  var fixedTop = function () {

  //将已有的视频换到主视频模式

  var els = document.getElementsByClassName('main_video');

  //小窗口排列

  var el_list = document.getElementsByClassName("video");

  

  console.error("视频数量==" + el_list.length);

  if (els== undefined||els == null ||els.length== 0){

    el_list[0].style.top="0px";

    el_list[0].className = 'main_video';

  }

  for (var i = 0; i < el_list.length; i++) {

    var el = el_list[i];

    el.style.top = (i * 80) + 'px';

    var p=el.querySelector(".licode_player");

    if(p) {

      p.ondblclick = function () {

        document.getElementsByClassName("main_video")[0].className = 'video';

        var pr = this.parentNode.parentNode;

        pr.className = 'main_video';

        pr.style.top = "0px";

        console.error(this.parentNode.id);

        fixedTop();

      }

    }

  }

}

//对应 export default methods的方法

//发布视频流

  subscribeToStreams(streams) {

  var cb = function (evt) {

    console.error('Bandwidth Alert', evt.msg, evt.bandwidth);

  };

  for (var index in streams) {

    var stream = streams[index];

    if (this.localStream.getID() !== stream.getID()) {

      this.room.subscribe(stream, {metadata: {type: 'subscriber'}});

      stream.addEventListener('bandwidth-alert', cb);

    }

  }

},
initLicode() {

  this.localStream = Erizo.Stream({

    audio: true, video: true, data: true, attributes: {id: this.currentUser.id, name: this.currentUser.name},

    desktopStreamId: this.currentUser.id, videoSize: [640, 480, 1920, 1080]//,videoFrameRate: [10, 20]

  });

  axios.defaults.headers.post['Content-Type'] = 'application/json';

  axios.defaults.headers.post['Accept'] = '*/*';

  axios.post(Constants.licodeServer + 'createToken/', {

    room: this.id,

    username: encodeURI(this.currentUser.name),

    role: 'presenter'

  }).then((resp) => {

    var token = resp.data;

    this.room = Erizo.Room({token: token});

      this.room.addEventListener('room-connected', (event) => {

        this.room.publish(this.localStream, {maxVideoBW: 2000, minVideoBW: 1000,metadata: {type: 'publisher'}});

        console.error("this.localStream room-connected id\t" + this.localStream.getID());

        this.subscribeToStreams(event.streams);

      });

  

      this.room.addEventListener('stream-subscribed', (streamEvent) => {

        var stream = streamEvent.stream;

        var attrs = stream.getAttributes();

        console.error("stream-subscribed\t接收视频流 id\t" + attrs.id);

        if (attrs.id == this.currentUser.id)

          return;

        var s = this.$el.querySelector(".main_video");

        if (s != undefined && s != null)

          s.className = "video";

  

        var elem = document.createElement("div");

        elem.setAttribute("id", attrs.id);

        elem.setAttribute("title", attrs.name);

        elem.className = "main_video";

        this.$el.querySelector("#video_list").appendChild(elem);

        var videoEl = document.createElement("div");

        videoEl.className = "videoPlay";

        videoEl.setAttribute("id", "video_" + attrs.id);

        elem.appendChild(videoEl);

        stream.show("video_" + attrs.id);

        // document.getElementById("audio_login").play();

        //移除licode logo

        document.getElementById("video_" + attrs.id).getElementsByClassName('licode_link')[0].innerHTML='';

        fixedTop();

      });

  

      this.room.addEventListener('stream-added', (event) => {

        console.error("stream-added id\t" + event.stream.getID());

        var streams = [];

        streams.push(event.stream);

        this.subscribeToStreams(streams);

      });

  

      this.room.addEventListener('stream-removed', (streamEvent) => {

        console.error("stream-removed");

        // Remove stream from DOM

        var stream = streamEvent.stream;

        if (stream.getID() !== undefined) {

          var attrs = stream.getAttributes();

          var element = document.getElementById(attrs.id);

          if (element != undefined && element != null)

            this.$el.querySelector("#video_list").removeChild(element);

          fixedTop();

        }

      });

      this.room.addEventListener('stream-failed', () => {

        console.error("stream-failed");

        this.room.disconnect();

      });

  

    this.localStream.addEventListener('access-accepted', () => {

      console.error("this.localStream access-accepted");

      this.room.connect();

      var attrs = this.localStream.getAttributes();

      var elem = document.createElement("div");

      elem.setAttribute("id", attrs.id);

      elem.setAttribute("title", attrs.name);

      elem.className = "main_video";

      this.$el.querySelector("#video_list").appendChild(elem);

      var videoEl = document.createElement("div");

      videoEl.setAttribute("id", "video_" + attrs.id);

      videoEl.className = "videoPlay";

      elem.appendChild(videoEl);

      this.localStream.show("video_" + attrs.id);//, {speaker: true});

      //移除licode logo

      document.getElementById("video_" + attrs.id).getElementsByClassName('licode_link')[0].innerHTML='';

      fixedTop();

    });

    this.localStream.init();

  });

},

  quitRoom() {

  if(this.localStream){}else{return;}

  this.localStream.stop();

  //取消订阅视频

  this.room.unsubscribe(this.localStream, (result, error) => {

    if (result === undefined) {

      console.error("Error unsubscribing", error);

    } else {

      console.error("Stream unsubscribed!");

    }

  });

  //取消发布视频

  this.room.unpublish(this.localStream, (result, error) => {

    if (result === undefined) {

      console.error("Error unpublishing", error);

    } else {

      console.error("Stream unpublished!");

    }

  });

  this.send({event:'leave'});

  this.room.disconnect();

  this.room = null;

  this.localStream = null;

}

 

会议聊天室说明:

    1. 初始化本地视频流
this.localStream = Erizo.Stream({

  audio: true, video: true, data: true, attributes: {id: this.currentUser.id, name: this.currentUser.name},

  desktopStreamId: this.currentUser.id, videoSize: [640, 480, 1920, 1080]//,videoFrameRate: [10, 20]

  });

 

    1. 获取聊天室token
    2. 在获取token成功后初始化room,对room设置事件监听:
      1. room-connected 聊天室连接成功

连接成功后发布本地视频流localStream

      1. stream-subscribed 视频流订阅成功

将获得的视频流播放, 通过fixedTop()方法设置位置,及设置主视频窗口。

      1. stream-added 视频流加入后进行整个room订阅视频流。并对此视频流设置bandwith-alert监听。
      2. stream-removed 视频流移除或断开监听

对已经断开的视频流的video进行移除,并且调用fixedTop来重新布局设置主次屏。

      1. stream-failed 视频连接失败监听

room断开连接。

      1. 对本地视频流监听,access-accepeted:

连接room:this.room.connect();

播放本地视频流,并且调用fixedTop来重新布局设置主次屏。

      1. 初始化localStream。
                       this.localStream.init();

转载于:https://my.oschina.net/loyin/blog/3043042

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值