WebRTC實現網頁版多人視頻聊天室

由於產品中要加入網頁中網絡會議的功能,這幾天都在倒騰 WebRTC,如今分享下工做成果。html

話說 WebRTC

Real Time Communication 簡稱 RTC,是谷歌若干年前收購的一項技術,後來把這項技術應用到瀏覽器中並開源出來,並且搞了一套標準提交給W3C,稱爲WebRTC,官方地址是:http://www.webrtc.org/。WebRTC要求瀏覽器內置實時傳輸音視頻的功能,並提供一致的API供JS使用。目前實現這套標準的瀏覽器有:Chrome、FireFox、Opera。微軟雖然也在對WebRTC標準的制定作貢獻,但仍然沒有在任何版本的IE中支持WebRTC,因此,對於IE瀏覽器,不得不安裝Chrome Frame插件來支持WebRTC;對於Safari瀏覽器,可使用WebRtc4all這個插件,地址是:https://code.google.com/p/webrtc4all/git

WebRTC基礎

WebRTC提供了三個API:MediaStream、RTCPeerConnection、RTCDataChannel。
  • MediaStream 用於獲取本地的 音視頻流。不一樣的瀏覽器名稱不同,但參數同樣,谷歌和Opera是navigator.webkitGetUserMedia,火狐是 navigator.mozGetUserMedia。
  • RTCPeerConnection:和 getUserMedia 同樣 谷歌和火狐分別會有webkit、moz前綴。這個對象主要用於兩個瀏覽器之間創建鏈接以及傳輸音視頻流。
  • RTCDataChannel 用於兩個瀏覽器之間傳輸自定義的數據,用這個對象能夠實現互發消息,而不用通過服務端的中轉。

WebRTC的實現是創建瀏覽器之間的直接鏈接,而不須要其餘服務器的中轉,即P2P,這就要求彼此之間須要知道對方的外網地址。但大多數計算機都位於NAT以後,只有少部分主機擁有外網地址,這就要求一種方式能夠穿透NAT,STUN和TURN就是這樣的技術。對於STUN和TURN的詳細介紹,能夠查看這裏(http://www.h3c.com.cn/MiniSite/Technology_Circle/Net_Reptile/The_Five/Home/Catalog/201206/747038_97665_0.htm)。github

WebRTC會使用默認的或程序指定的SUTN服務器,獲取指向當前主機的外網地址和端口。谷歌瀏覽器默認的是谷歌域名下的一個STUN,國內可能不大穩定,因而我找到了這個 stunserver.org/ ,鏈接速度比較快,聽說當年飛信就是使用的這個,應該比較可靠。若是信不過第三方的STUN服務,也能夠本身搭建一臺,搭建過程也挺簡單。web

P2P的創建過程須要依賴服務端中轉外網IP及端口、音視頻設備配置信息,因此服務端須要使用能夠雙工通信的手段,好比WebSocket,來實現信令的中轉,稱之爲信令服務器。api

WebRTC會話的創建詳解

會話的創建主要有兩個過程:網絡信息的交換、音視頻設備信息的交換。如下以 lilei 要和 Lucy 開視頻爲例描述這兩個過程。瀏覽器

網絡信息的交換:服務器

  1. lilei首先建立了一個RTCPeerConnection對象,這個對象會自動的去向STUN服務器詢問本身的外網IP和端口。而後lilei把本身的網絡信息通過信令服務器中轉後,發送給lucy。
  2. lucy接收到lilei的網絡信息以後,也建立了一個RTCPeerConnection對象,並把lilei發過來的信息經過addIceCandidate添加到對象中。
  3. lucy把本身的網絡信息通過信令服務器的中轉後,發送給lilei。
  4. lilei接收到信息後,經過RTCPeerConnection對象的addIceCandidate方法保存lucy的網絡信息。

音視頻設備信息的交換:網絡

  1. lilei經過RTCPeerConnection對象的createOffer方法,獲取本地的音視頻編碼分辨率等信息,經過setLocalDescription添加到RTCPeerConnection中,並把這些信息通過信令服務器中轉後發送給lucy。
  2. lucy接收到lilei發過來的信息後,使用RTCPeerConnection對象的setRemoteDescription方法保存。而後經過createAnswer方法獲取本身的音視頻信息並以一樣的手段發送給lilei。
  3. lilei接收到lucy的信 息,調用setRemoteDescription方法保存。

以上兩個過程能夠是併發的,並沒有前後順序,但必須得等到兩個過程都完成後,P2P的鏈接才真正的創建。一旦鏈接創建,lilei和lucy就能夠直接發送音視頻流,而不須要中轉。WebRTC在獲取本地網絡信息的時候,會先嚐試STUN,若是失敗,則會使用TURN。session

WebRTC + Asp.net Web API 實現視頻聊天室

首先使用WebSocket實現信令服務器部分,在此須要用到微軟開發的用於實現WebSocket的dll (http://www.nuget.org/packages/Microsoft.WebSockets/),以及Json.net。併發

用於和客戶端交互的會話類代碼以下:
    public class Session : WebSocketHandler
    {
        private static WebSocketCollection sessions = new WebSocketCollection();

        public String UserId { get; set; }

        public override void OnOpen()
        {
            this.UserId = Guid.NewGuid().ToString("N");
            var message = new { type = SignalMessageType.Conect, userId = this.UserId };
            sessions.Broadcast(Json.Encode(message));

            sessions.Add(this);
        }

        public override void OnMessage(string msg)
        {
            var obj = Json.Decode(msg);
            var messageType = (SignalMessageType)obj.type;

            switch (messageType)
            {
                case SignalMessageType.Offer:
                case SignalMessageType.Answer:
                case SignalMessageType.IceCandidate:
                    var session = sessions.Cast<Session>().FirstOrDefault(n => n.UserId == obj.userId);
                    var message = new { type = messageType, userId = this.UserId, description = obj.description };
                    session.Send(Json.Encode(message));
                    break;
            }
        }
    }

    public enum SignalMessageType
    {
        Conect,
        DisConnect,
        Offer,
        Answer,
        IceCandidate
    }
View Code

WebAPI控制器須要引用命名空間「Microsoft.Web.WebSockets;」代碼以下:

    public class SignalServerController : ApiController
    {
        [HttpGet]
        public HttpResponseMessage Connect()
        {
            var session = new WebRTCDemo.Session();
            HttpContext.Current.AcceptWebSocketRequest(session);
 
            return new HttpResponseMessage(HttpStatusCode.SwitchingProtocols);
        }
    }
View Code
JS腳本:
var RtcConnect = function (_userId, _webSocketHelper) {
 
    var config = { iceServers: [{ url: 'stun:stunserver.org' }] };
    var peerConnection = null;
    var userId = _userId;
    var webSocketHelper = _webSocketHelper;
 
    var createVideo = function (stream) {
        var src = window.webkitURL.createObjectURL(stream);
        var video = $("<video />").attr("src", src);
        var container = $("<div />").addClass("videoContainer").append(video).appendTo($("body"));
 
        video[0].play();
        return container;
    };
 
    var init = function () {
 
        window.RTCPeerConnection = window.RTCPeerConnection || window.webkitRTCPeerConnection || window.mozRTCPeerConnection;
        peerConnection = window.RTCPeerConnection(config);
 
        peerConnection.addEventListener('addstream', function (event) {
            createVideo(event.stream);
        });
        peerConnection.addEventListener('icecandidate', function (event) {
            var description = JSON.stringify(event.candidate);
            var message = JSON.stringify({ type: 4, userId: userId, description: description });
            webSocketHelper.send(message);
        });
 
        navigator.getMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia;
        var localStream = navigator.getMedia({ video: true, audio: true }, getUserMediaSuccess, getUserMediaFail);
        peerConnection.addStream(localStream);
 
    };
 
    this.connect = function () {
        peerConnection.createOffer(function (offer) {
            peerConnection.setLocalDescription(offer);
 
            var description = JSON.stringify(offer);
            var message = JSON.stringify({ type: 2, userId: userId, description: description });
            webSocketHelper.send(message);
        });
 
    };
 
    this.acceptOffer = function (offer) {
        peerConnection.setRemoteDescription(new RTCSessionDescription(offer));
        peerConnection.createAnswer(function (answer) {
            peerConnection.setLocalDescription(answer);
            var description = JSON.stringify(answer);
 
            var message = JSON.stringify({ type: 3, userId: userId, description: description });
            webSocketHelper.send(message);
        });
    };
 
    this.acceptAnswer = function (answer) {
        peerConnection.setRemoteDescription(new RTCSessionDescription(answer));
 
    };
 
    this.addIceCandidate = function (candidate) {
        peerConnection.addIceCandidate(new RTCIceCandidate(candidate));
    };
 
    init();
 
};
 
var WebSocketHelper = function (callback) {
    var ws = null;
    var url = "ws://" + document.location.host + "/api/Signal/Connect";
 
    var init = function () {
        ws = new WebSocket(url);
        ws.onmessage = onmessage;
        ws.onerror = onerror;
        ws.onopen = onopen;
    };
 
    var onmessage = function (message) {
        callback(JSON.parse(message.data));
    };
 
    this.send = function (data) {
        ws.send(data);
    };
 
    init();
};
 
$(function() {
 
    var rtcConnects = {};
    var webSocketHelper = new WebSocketHelper(function (message) {
        var rtcConnect = getOrCreateRtcConnect(message.userId);
        switch (message.type) {
            case 0: //Conect
                rtcConnect.connect();
                break;
            case 2: //Offer
                rtcConnect.acceptOffer(JSON.parse(message.description));
                break;
            case 3: //Answer
                rtcConnect.acceptAnswer(JSON.parse(message.description));
                break;
            case 4: //IceCandidate
                rtcConnect.addIceCandidate(JSON.parse(message.description));
                break;
            default:
                break;
        }
    });
 
    var init = function() {
        navigator.getMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia;
        var stream = navigator.getMedia({ video: true, audio: true }, function() {
            var src = window.webkitURL.createObjectURL(stream);
            var video = $("<video />").attr("src", src);
            $("<div />").addClass("videoContainer").append(video).appendTo($("body"));
 
            video[0].play();
        }, function (error) { console.error(error); });
    };
 
    var getOrCreateRtcConnect = function (userId) {
        var rtcConnect = rtcConnects[userId];
        if (typeof (rtcConnect) == 'undefined') {
            rtcConnect = new rtcConnect(userId, webSocketHelper);
            rtcConnects[userId] = rtcConnect;
        }
        return rtcConnect;
    };
    init();
});
View Code
View代碼:
<html>
<head>
    <style>
        .videoContainer { float: left; padding: 10px 0 10px 10px; width: 210px; margin: 5px; }
        .videoContainer > video { width: 200px; height: 150px; margin-top: 5px; }
    </style>
</head>
<body>
</body>
</html>
View Code
編譯後部署到IIS上,讓同事都來試試,略有激動。

其餘

若是想部署本身專用的STUN服務器,這裏(http://www.stunprotocol.org/)有STUN服務器的完整開源實現,原生是運行在Linux上的,但也提供了cgwin下編譯的windwos版本。如何編譯、運行等在它的github主頁上說的比較清楚:https://github.com/jselbie/stunserver

若是以爲本身寫那一坨js比較繁瑣,這裏(http://www.rtcmulticonnection.org/)有一個封裝庫,簡單瞭解了一下,功能挺強大的。

相關文章
相關標籤/搜索