使用 WebRTC 構建簡單的前端視頻通訊


在傳統的 Web 應用中,瀏覽器與瀏覽器之間是無法直接相互通信的,必須借助服務器的幫助,但是隨着 WebRTC 在各大瀏覽器中的普及,這一現狀得到了改變。

WebRTC(Web Real-Time Communication,Web實時通信),是一個支持網頁瀏覽器之間進行實時數據傳輸(包括音頻、視頻、數據流)的技術,谷歌於2011年5月開放了工程的源代碼,目前在各大瀏覽器的最新版本中都得到了不同程度的支持。

這篇文章里我們采用 WebRTC 來構建一個簡單的視頻傳輸應用。

一、關於 WebRTC 的一些基本概念

傳統的視頻推流的技術實現一般是這樣的:客戶端采集視頻數據,推流到服務器上,服務器再根據具體情況將視頻數據推送到其他客戶端上。

但是 WebRTC 卻截然不同,它可以在客戶端之間直接搭建基於 UDP 的數據通道,經過簡單的握手流程之后,可以在不同設備的兩個瀏覽器內直接傳輸任意數據。

這其中的流程包括:

  1. 采集視頻流數據,創建一個 RTCPeerConnection

  2. 創建一個 SDP offer 和相應的回應

  3. 為雙方找到 ICE 候選路徑

  4. 成功創建一個 WebRTC 連接

下面我們介紹這其中涉及到的一些關鍵詞:

1、RTCPeerConnection 對象

RTCPeerConnection 對象是 WebRTC API 的入口,它負責創建、維護一個 WebRTC 連接,以及在這個連接中的數據傳輸。目前新版本的瀏覽器大都支持了這一對象,但是由於目前 API 還不穩定,所以需要加入各個瀏覽器內核的前綴,例如 Chrome 中我們使用 webkitRTCPeerConnection 來訪問它。

2、會話描述協議(SDP)

為了連接到其他用戶,我們必須要對其他用戶的設備情況有所了解,比如音頻視頻的編碼解碼器、使用何種編碼格式、使用何種網絡、設備的數據處理能力,所以我們需要一張“名片”來獲得用戶的所有信息,而 SDP 為我們提供了這些功能。

一個 SDP 的握手由一個 offer 和一個 answer 組成。

3、網絡通信引擎(ICE)

通信的兩側可能會處於不同的網絡環境中,有時會存在好幾層的訪問控制、防火牆、路由跳轉,所以我們需要一種方法在復雜的網絡環境中找到對方,並且連接到相應的目標。WebRTC 使用了集成了 STUN、TURN 的 ICE 來進行雙方的數據通信。

二、創建一個 RTCPeerConnection

首先我們的目標是在同一個頁面中創建兩個實時視頻,一個的數據直接來自你的攝像頭,另一個的數據來自本地創建的 WebRTC 連接。看起來是這樣的:

圖圖圖。。。。。。。

首先我們創建一個簡單的 HTML 頁面,含有兩個 video 標簽:

<!DOCTYPE html>
<html>
<head>
    <title></title>
    <style type="text/css">
        #theirs{
            position: absolute;
            top: 100px;
            left: 100px;
            width: 500px;
        }
        #yours{
            position: absolute;
            top: 120px;
            left: 480px;
            width: 100px;
            z-index: 9999;
            border:1px solid #ddd;
        }
    </style>
</head>
<body>
<video id="yours" autoplay></video>
<video id="theirs" autoplay></video>
</body>
<script type="text/javascript" src="./main.js"></script>
</html>

  

下面我們創建一個 main.js 文件,先封裝一下各瀏覽器的 userMedia 和 RTCPeerConnection 對象:

function hasUserMedia() {
    navigator.getUserMedia = navigator.getUserMedia || navigator.msGetUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia;
    return !!navigator.getUserMedia;
}

function hasRTCPeerConnection() {
    window.RTCPeerConnection = window.RTCPeerConnection || window.webkitRTCPeerConnection || window.mozRTCPeerConnection || window.msRTCPeerConnection;
    return !!window.RTCPeerConnection;
}

  

然后我們需要瀏覽器調用系統的攝像頭 API getUserMedia 獲得媒體流,注意要打開瀏覽器的攝像頭限制。Chrome由於安全的問題,只能在 https 下或者 localhost 下打開攝像頭。

var yourVideo = document.getElementById("yours");
var theirVideo = document.getElementById("theirs");
var yourConnection, theirConnection;

if (hasUserMedia()) {
    navigator.getUserMedia({ video: true, audio: false },
        stream => {
            yourVideo.src = window.URL.createObjectURL(stream);
            if (hasRTCPeerConnection()) {
                // 稍后我們實現 startPeerConnection
                startPeerConnection(stream);
            } else {
                alert("沒有RTCPeerConnection API");
            }
        },
        err => {
            console.log(err);
        }
    )
}else{
    alert("沒有userMedia API")
}

  

沒有意外的話,現在應該能在頁面中看到一個視頻了。

下一步是實現 startPeerConnection 方法,建立傳輸視頻數據所需要的 ICE 通信路徑,這里我們以 Chrome 為例:

function startPeerConnection(stream) {
    //這里使用了幾個公共的stun協議服務器
    var config = {
        'iceServers': [{ 'url': 'stun:stun.services.mozilla.com' }, { 'url': 'stun:stunserver.org' }, { 'url': 'stun:stun.l.google.com:19302' }]
    };
    yourConnection = new RTCPeerConnection(config);
    theirConnection = new RTCPeerConnection(config);
    yourConnection.onicecandidate = function(e) {
        if (e.candidate) {
            theirConnection.addIceCandidate(new RTCIceCandidate(e.candidate));
        }
    }
    theirConnection.onicecandidate = function(e) {
        if (e.candidate) {
            yourConnection.addIceCandidate(new RTCIceCandidate(e.candidate));
        }
    }
}

  

我們使用這個函數創建了兩個連接對象,在 config 里,你可以任意指定 ICE 服務器,雖然有些瀏覽器內置了默認的 ICE 服務器,可以不用配置,但還是建議加上這些配置。下面,我們進行 SDP 的握手。

由於是在同一頁面中進行的通信,所以我們可以直接交換雙方的 candidate 對象,但在不同頁面中,可能需要一個額外的服務器協助這個交換流程。

三、建立 SDP Offer 和 SDP Answer

瀏覽器為我們封裝好了相應的 Offer 和 Answer 方法,我們可以直接使用。

function startPeerConnection(stream) {
    var config = {
        'iceServers': [{ 'url': 'stun:stun.services.mozilla.com' }, { 'url': 'stun:stunserver.org' }, { 'url': 'stun:stun.l.google.com:19302' }]
    };
    yourConnection = new RTCPeerConnection(config);
    theirConnection = new RTCPeerConnection(config);
    yourConnection.onicecandidate = function(e) {
        if (e.candidate) {
            theirConnection.addIceCandidate(new RTCIceCandidate(e.candidate));
        }
    }
    theirConnection.onicecandidate = function(e) {
        if (e.candidate) {
            yourConnection.addIceCandidate(new RTCIceCandidate(e.candidate));
        }
    }

    //本方產生了一個offer
    yourConnection.createOffer().then(offer => {
        yourConnection.setLocalDescription(offer);
        //對方接收到這個offer
        theirConnection.setRemoteDescription(offer);
        //對方產生一個answer
        theirConnection.createAnswer().then(answer => {
            theirConnection.setLocalDescription(answer);
            //本方接收到一個answer
            yourConnection.setRemoteDescription(answer);
        })
    });

}

  

和 ICE 的連接一樣,由於我們是在同一個頁面中進行 SDP 的握手,所以不需要借助任何其他的通信手段來交換 offer 和 answer,直接賦值即可。如果需要在兩個不同的頁面中進行交換,則需要借助一個額外的服務器來協助,可以采用 websocket 或者其它手段進行這個交換過程。

四、加入視頻流

現在我們已經有了一個可靠的視頻數據傳輸通道了,下一步只需要向這個通道加入數據流即可。WebRTC 直接為我們封裝好了加入視頻流的接口,當視頻流添加時,另一方的瀏覽器會通過 onaddstream 來告知用戶,通道中有視頻流加入。

yourConnection.addStream(stream);
theirConnection.onaddstream = function(e) {
    theirVideo.src = window.URL.createObjectURL(e.stream);
}

  

以下是完整的 main.js 代碼:

function hasUserMedia() {
    navigator.getUserMedia = navigator.getUserMedia || navigator.msGetUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia;
    return !!navigator.getUserMedia;
}
function hasRTCPeerConnection() {
    window.RTCPeerConnection = window.RTCPeerConnection || window.webkitRTCPeerConnection || window.mozRTCPeerConnection || window.msRTCPeerConnection;
    return !!window.RTCPeerConnection;
}

var yourVideo = document.getElementById("yours");
var theirVideo = document.getElementById("theirs");
var yourConnection, theirConnection;

if (hasUserMedia()) {
    navigator.getUserMedia({ video: true, audio: false },
        stream => {
            yourVideo.src = window.URL.createObjectURL(stream);
            if (hasRTCPeerConnection()) {
                startPeerConnection(stream);
            } else {
                alert("沒有RTCPeerConnection API");
            }
        },
        err => {
            console.log(err);
        })
} else {
    alert("沒有userMedia API")
}

function startPeerConnection(stream) {
    var config = {
        'iceServers': [{ 'url': 'stun:stun.services.mozilla.com' }, { 'url': 'stun:stunserver.org' }, { 'url': 'stun:stun.l.google.com:19302' }]
    };
    yourConnection = new RTCPeerConnection(config);
    theirConnection = new RTCPeerConnection(config);

    yourConnection.onicecandidate = function(e) {
        if (e.candidate) {
            theirConnection.addIceCandidate(new RTCIceCandidate(e.candidate));
        }
    }
    theirConnection.onicecandidate = function(e) {
        if (e.candidate) {
            yourConnection.addIceCandidate(new RTCIceCandidate(e.candidate));
        }
    }
    
    theirConnection.onaddstream = function(e) {
        theirVideo.src = window.URL.createObjectURL(e.stream);
    }
    yourConnection.addStream(stream);
    
    yourConnection.createOffer().then(offer => {
        yourConnection.setLocalDescription(offer);
        theirConnection.setRemoteDescription(offer);
        theirConnection.createAnswer().then(answer => {
            theirConnection.setLocalDescription(answer);
            yourConnection.setRemoteDescription(answer);
        })
    });
}

  


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM