首頁>技術>

系統說明:

本系統是基於谷歌的webRTC瀏覽器端影片點對點技術實現網頁影片及語音即時通訊。採用開源專案licode作為聊天室管理,以及其web端js模組。

目前系統採用前後端分離以及多伺服器方式部署,採用nginx作為反向代理整合licode、apiserver、front。

環境要求:

作業系統:ubuntu14、nginx、nodejs、RibbitMQ

Licode安裝

在ubuntu中安裝:

先從GitHub上下載原始碼

git clone https://github.com/ging/licode.gitcd licode

解決HTTPS/SSL訪問錯誤:

由於是配置檔名不統一導致,而且在Sokect.js中的token.secure引數無法配置,故直接修改。

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

將licode_default.js檔案修改SSL相關配置,注意ssl證書也需要修改

ssl相關設定為true;

然後複製licode_default.js為licode_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、安裝系統依賴

./scripts/installUbuntuDeps.sh

2、安裝Erizo和Nuve

./scripts/installErizo.sh./scripts/installNuve.sh

3、安裝基本例項程式

./scripts/installBasicExample.sh

4、執行licode

./licode/scripts/initLicode.sh

5、執行基礎例項web服務

./scripts/initBasicExample.shLicode 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

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

// 配置IP,EC2下需要配置listening-ip和external-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的能力,但是需要給它配置2個IP,以幫助探測客戶端所在NAT環境的行為,這裡沒有做。

前端整合時序圖

WebRTC相關時序:

瀏覽器透過candidate 來進行連線。連線之後,就無需與伺服器進行連線。

在獲得candidate時需要連線turnserver。具體是由瀏覽器webRTC模組執行。

Licode整合時序圖:

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

}

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

前端整合

需要在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!=''&¶ms.roomId!='-'&¶ms.roomId!='null'&¶ms.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;}

會議聊天室說明:

初始化本地影片流
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]  });
獲取聊天室token在獲取token成功後初始化room,對room設定事件監聽:room-connected 聊天室連線成功

連線成功後釋出本地影片流localStream

stream-subscribed 影片流訂閱成功

將獲得的影片流播放, 透過fixedTop()方法設定位置,及設定主影片視窗。

stream-added 影片流加入後進行整個room訂閱影片流。並對此影片流設定bandwith-alert監聽。stream-removed 影片流移除或斷開監聽

對已經斷開的影片流的video進行移除,並且呼叫fixedTop來重新佈局設定主次屏。

stream-failed 影片連線失敗監聽

room斷開連線。

對本地影片流監聽,access-accepeted:

連線room:this.room.connect();

播放本地影片流,並且呼叫fixedTop來重新佈局設定主次屏。

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

14
  • BSA-TRITC(10mg/ml) TRITC-BSA 牛血清白蛋白改性標記羅丹明
  • Python 程式碼最佳化,讓你的Pandas速度飛起來