WebRTC(Web Real-Time Communication)協議
WebRTC(Web Real-Time Communication)是一種支持瀏覽器和移動應用程序之間進行 實時音頻、視頻和數據通信 的協議。它使得開發者能夠在瀏覽器中實現高質量的 P2P(點對點)實時通信,而無需安裝插件或第三方軟件。WebRTC 主要用于視頻通話、語音聊天、在線協作和數據傳輸等應用場景。
核心功能
WebRTC 提供了以下幾個關鍵功能:
- 音視頻通信:WebRTC 支持通過瀏覽器進行音頻和視頻的實時傳輸,用戶之間可以進行高質量的語音和視頻通話。
- P2P 數據傳輸:WebRTC 不僅支持音視頻流,還支持點對點的數據通道(Data Channel)傳輸,適合進行文件傳輸、屏幕共享和實時協作。
- 低延遲:WebRTC 專門優化了數據傳輸過程,以減少通信中的延遲,使得實時通信更加順暢。
工作原理
WebRTC 使用了一系列底層協議和技術來實現點對點通信。WebRTC 的工作流程通常包括以下幾個關鍵步驟:
- 建立連接(Signaling)
- 在 WebRTC 中,信令(Signaling) 是客戶端用于交換通信所需的元數據(如網絡信息、音視頻編解碼信息、媒體能力等)的一種過程。信令不是 WebRTC 協議的一部分,但它是 WebRTC 通信的必要步驟。
- 信令的內容包括:協商媒體(音視頻)格式、網絡路徑、設備信息等。
- 通常,信令使用 WebSockets、HTTP 或其他協議進行實現。
- 信令過程包括:
- Offer(提議):發起方創建會話請求,發送給接收方。
- Answer(應答):接收方回應發起方的請求,確認會話設置。
- ICE candidates(ICE 候選者):每個端點通過收集網絡候選地址來交換,以幫助建立最佳的 P2P 連接。
- 網絡連接(ICE、STUN 和 TURN)
- ICE(Interactive Connectivity Establishment):用于在 NAT 后的網絡環境中建立端到端的連接。ICE 是 WebRTC 連接的關鍵組成部分,它幫助客戶端發現并連接到彼此。
- STUN(Session Traversal Utilities for NAT):STUN 服務器幫助客戶端了解自己在 NAT 后的公網 IP 地址。
- TURN(Traversal Using Relays around NAT):TURN 服務器在 P2P 連接無法直接建立時提供數據轉發服務,確保通信的可靠性。TURN 作為最后的解決方案,通常會導致更高的延遲,因此只有在需要時才使用。
- 媒體流傳輸(RTP/RTCP)
- RTP(Real-Time Transport Protocol):RTP 是 WebRTC 用于音頻和視頻流的傳輸協議。它允許在網絡中實時地傳輸數據包,并為這些數據包添加時間戳,確保音視頻數據的正確順序。
- RTCP(Real-Time Control Protocol):RTCP 用于監控 RTP 會話的質量,并提供流控制和同步。
- 數據傳輸(DataChannel)
- RTCDataChannel:WebRTC 支持數據通道(DataChannel),使得瀏覽器間可以通過 P2P 傳輸任意數據(包括文本、文件、圖像等)。數據通道提供了低延遲的點對點數據傳輸能力,常用于文件傳輸、屏幕共享等應用。
關鍵技術
WebRTC 由多種技術組成,其中最重要的包括:
-
getUserMedia:用于獲取用戶的音頻和視頻輸入設備(如麥克風和攝像頭)的權限。它會返回一個包含音視頻流的對象。
navigator.mediaDevices.getUserMedia({ video: true, audio: true }).then(stream => {// 顯示視頻流const videoElement = document.getElementById('my-video');videoElement.srcObject = stream;}).catch(error => console.log('Error accessing media devices: ', error));
-
RTCPeerConnection:用于建立、維護和管理 P2P 連接。它負責處理網絡連接、音視頻編解碼、帶寬管理等任務。
const peerConnection = new RTCPeerConnection(configuration); peerConnection.addStream(localStream); // 添加本地音視頻流// 建立連接后,發送媒體流 peerConnection.createOffer().then(offer => peerConnection.setLocalDescription(offer)).then(() => {// 將 offer 發送給接收方});
-
RTCDataChannel:用于建立點對點的數據傳輸通道,可以傳輸任意類型的數據。
javascript復制編輯const dataChannel = peerConnection.createDataChannel('chat'); dataChannel.onopen = () => console.log('Data channel open'); dataChannel.onmessage = (event) => console.log('Received message: ', event.data);// 發送數據 dataChannel.send('Hello, WebRTC!');
應用場景
- 視頻通話:WebRTC 可以用于構建視頻會議應用,如 Zoom、Google Meet 等。
- 語音通話:WebRTC 支持語音通話,廣泛應用于 IP 電話、語音助手等。
- 文件傳輸:通過 RTCDataChannel,WebRTC 可以用于點對點的文件傳輸。
- 實時協作:WebRTC 用于多人在線編輯、白板共享等實時協作工具。
- 直播:WebRTC 可以支持低延遲的視頻直播,適用于游戲直播、網絡教學等領域。
實現過程
1. 獲取音視頻流(getUserMedia)
getUserMedia
是 WebRTC 中用于訪問用戶音頻和視頻設備的 API。通過它,你可以獲取麥克風和攝像頭的權限,從而獲取用戶的音視頻流。
示例:獲取視頻和音頻流
navigator.mediaDevices.getUserMedia({ video: true, audio: true }).then(stream => {// 獲取視頻流后,可以將其顯示在視頻標簽上const videoElement = document.getElementById('localVideo');videoElement.srcObject = stream;// 創建 RTCPeerConnection 實例(將在后面討論)const peerConnection = new RTCPeerConnection();// 將本地流添加到連接stream.getTracks().forEach(track => peerConnection.addTrack(track, stream));}).catch(error => {console.error('Error accessing media devices.', error);});
getUserMedia
:請求用戶設備的音視頻流。- 返回的
MediaStream
可以用于顯示、錄制或傳輸。
2. 創建點對點連接(RTCPeerConnection)
WebRTC 使用 RTCPeerConnection 來管理媒體流的傳輸。它代表了與另一個客戶端的點對點連接。
示例:創建 RTCPeerConnection 并添加本地流
const peerConnection = new RTCPeerConnection({iceServers: [{ urls: 'stun:stun.l.google.com:19302' } // 使用 STUN 服務器]
});// 添加本地流到連接
navigator.mediaDevices.getUserMedia({ video: true, audio: true }).then(stream => {const localVideo = document.getElementById('localVideo');localVideo.srcObject = stream;stream.getTracks().forEach(track => peerConnection.addTrack(track, stream));});
- STUN 服務器:STUN(Session Traversal Utilities for NAT)幫助客戶端發現自己的公共 IP 地址,用于 NAT 穿透。
3. 信令交換(Signal)
WebRTC 協議并不直接定義信令交換的方式,因此你需要自己實現信令交換。信令過程用于交換連接的元數據,如會話描述(SDP)和 ICE 候選者等。
- 創建 Offer(發起方)
peerConnection.createOffer().then(offer => {return peerConnection.setLocalDescription(offer); // 設置本地 SDP}).then(() => {// 將 offer 發送給對方(通過信令服務器)signalingServer.send({ type: 'offer', offer: peerConnection.localDescription });});
- SDP(Session Description Protocol):描述了音視頻流的編碼、傳輸等信息。
- 設置 Answer(接收方)
接收方收到 Offer 后,創建 Answer 并回復:
signalingServer.on('offer', offer => {peerConnection.setRemoteDescription(new RTCSessionDescription(offer)).then(() => peerConnection.createAnswer()).then(answer => {return peerConnection.setLocalDescription(answer); // 設置本地 SDP}).then(() => {// 將 answer 發送給發起方signalingServer.send({ type: 'answer', answer: peerConnection.localDescription });});
});
- 交換 ICE 候選者
在連接過程中,客戶端會收集并交換 ICE 候選者(候選網絡路徑)。這些候選者用于尋找最佳的連接路徑。
peerConnection.onicecandidate = event => {if (event.candidate) {// 發送 ICE 候選者到對方signalingServer.send({ type: 'ice-candidate', candidate: event.candidate });}
};// 接收對方的 ICE 候選者
signalingServer.on('ice-candidate', candidate => {peerConnection.addIceCandidate(new RTCIceCandidate(candidate));
});
- 建立連接并處理媒體流
當信令交換完成并且 ICE 候選者交換完畢后,WebRTC 將會建立一個完整的點對點連接,音視頻流會開始傳輸。
示例:顯示遠端視頻流
peerConnection.ontrack = event => {const remoteVideo = document.getElementById('remoteVideo');remoteVideo.srcObject = event.streams[0]; // 獲取遠程流并顯示
};
5. 數據通道(RTCDataChannel)
WebRTC 還支持 RTCDataChannel,用于在兩個客戶端之間進行低延遲的點對點數據傳輸(例如文件傳輸、聊天信息等)。
示例:創建并使用數據通道
const dataChannel = peerConnection.createDataChannel('chat');// 監聽數據通道的消息
dataChannel.onmessage = event => {console.log('Received message:', event.data);
};// 發送數據
dataChannel.send('Hello from WebRTC!');
6. 斷開連接
當通信結束時,你可以通過關閉 PeerConnection 來斷開連接并釋放資源。
peerConnection.close();
WebRTC-Streamer開源項目
項目介紹
WebRTC-Streamer 是一個開源工具集,旨在簡化實時音視頻數據流的傳輸與集成,主要通過 WebRTC 技術實現低延遲的音視頻流傳輸。開發者無需深入理解復雜的底層協議即可輕松將實時音視頻功能集成到自己的應用中。該項目特別設計了高效的音視頻流處理功能,支持多種數據來源,如 V4L2 捕獲設備、RTSP 流、屏幕捕捉 等,適用于多種實時傳輸場景。
快速啟動
WebRTC-Streamer 提供了簡便的集成方式。以下是一個通過 HTML 和 JavaScript 快速搭建基本實時音視頻流服務的示例代碼:
<html>
<head><script src="libs/adapter.min.js"></script><script src="webrtcstreamer.js"></script>
</head>
<body>
<script>
var webRtcServer;
window.onload = function() {webRtcServer = new WebRtcStreamer(document.getElementById("video"), location.protocol+"//" + location.hostname + ":8000");webRtcServer.connect("rtsp://wowzaec2demo.streamlock.net/vod/mp4:BigBuckBunny_115k.mov");
}
window.onbeforeunload = function() {if (webRtcServer !== null) {webRtcServer.disconnect();}
}
</script>
<video id="video" controls autoplay muted></video>
</body>
</html>
代碼解析:
- 引入了
adapter.min.js
和webrtcstreamer.js
兩個必要的 JavaScript 庫。 - 創建一個
WebRtcStreamer
實例,指定本地服務器地址及目標 RTSP 視頻流地址。 - 頁面加載時自動連接至 RTSP 流,播放視頻。
- 頁面卸載時,斷開連接,釋放資源。
應用案例與最佳實踐
示例 1:直播演示
- 使用 WebRTC-Streamer 可以通過簡化的 Web 組件方式輕松展示來自 RTSP 源的實時視頻流。
<webrtc-streamer url="rtsp://wowzaec2demo.streamlock.net/vod/mp4:BigBuckBunny_115k.mov">
示例 2:地圖上的直播流
- 配合 Google Map API,WebRTC-Streamer 可以在地圖上顯示多個實時視頻流,適用于 監控、交通管理 等領域。
其它生態項目
WebRTC-Streamer 還支持一系列相關生態項目,以擴展其功能和適用范圍:
- webrtc-streamer-card:為 Home Assistant 提供的卡片插件,允許從 WebRTC-Streamer 服務中拉取零延遲視頻流,適用于智能家居。
- rpi-webrtc-streamer:面向 樹莓派 系列微控制器的 WebRTC 流媒體軟件包,支持在邊緣設備上實現高效的音視頻處理。
- Live555 Integration:通過整合 Live555 Media Server,增強 WebRTC-Streamer 在處理非標準音視頻格式方面的能力,擴展其應用場景。
附錄:WebRTC-Streamer項目地址
https://gitcode.com/gh_mirrors/we/webrtc-streamer/?utm_source=artical_gitcode&index=bottom&type=card&webUrl&isLogin=1
附錄:webrtcstreamer.js源碼
// webrtcstreamer.js
var WebRtcStreamer = (function() {/** * Interface with WebRTC-streamer API* @constructor* @param {string} videoElement - id of the video element tag* @param {string} srvurl - url of webrtc-streamer (default is current location)*/var WebRtcStreamer = function WebRtcStreamer (videoElement, srvurl) {if (typeof videoElement === "string") {this.videoElement = document.getElementById(videoElement);} else {this.videoElement = videoElement;}this.srvurl = srvurl || location.protocol+"//"+window.location.hostname+":"+window.location.port;this.pc = null; this.mediaConstraints = { offerToReceiveAudio: true, offerToReceiveVideo: true };this.iceServers = null;this.earlyCandidates = [];}WebRtcStreamer.prototype._handleHttpErrors = function (response) {if (!response.ok) {throw Error(response.statusText);}return response;}/** * Connect a WebRTC Stream to videoElement * @param {string} videourl - id of WebRTC video stream* @param {string} audiourl - id of WebRTC audio stream* @param {string} options - options of WebRTC call* @param {string} stream - local stream to send*/WebRtcStreamer.prototype.connect = function(videourl, audiourl, options, localstream) {this.disconnect();// getIceServers is not already receivedif (!this.iceServers) {console.log("Get IceServers");fetch(this.srvurl + "/api/getIceServers").then(this._handleHttpErrors).then( (response) => (response.json()) ).then( (response) => this.onReceiveGetIceServers(response, videourl, audiourl, options, localstream)).catch( (error) => this.onError("getIceServers " + error ))} else {this.onReceiveGetIceServers(this.iceServers, videourl, audiourl, options, localstream);}}/** * Disconnect a WebRTC Stream and clear videoElement source*/WebRtcStreamer.prototype.disconnect = function() { if (this.videoElement?.srcObject) {this.videoElement.srcObject.getTracks().forEach(track => {track.stop()this.videoElement.srcObject.removeTrack(track);});}if (this.pc) {fetch(this.srvurl + "/api/hangup?peerid=" + this.pc.peerid).then(this._handleHttpErrors).catch( (error) => this.onError("hangup " + error ))try {this.pc.close();}catch (e) {console.log ("Failure close peer connection:" + e);}this.pc = null;}} /** GetIceServers callback*/WebRtcStreamer.prototype.onReceiveGetIceServers = function(iceServers, videourl, audiourl, options, stream) {this.iceServers = iceServers;this.pcConfig = iceServers || {"iceServers": [] };try { this.createPeerConnection();var callurl = this.srvurl + "/api/call?peerid=" + this.pc.peerid + "&url=" + encodeURIComponent(videourl);if (audiourl) {callurl += "&audiourl="+encodeURIComponent(audiourl);}if (options) {callurl += "&options="+encodeURIComponent(options);}if (stream) {this.pc.addStream(stream);}// clear early candidatesthis.earlyCandidates.length = 0;// create Offerthis.pc.createOffer(this.mediaConstraints).then((sessionDescription) => {console.log("Create offer:" + JSON.stringify(sessionDescription));this.pc.setLocalDescription(sessionDescription).then(() => {fetch(callurl, { method: "POST", body: JSON.stringify(sessionDescription) }).then(this._handleHttpErrors).then( (response) => (response.json()) ).catch( (error) => this.onError("call " + error )).then( (response) => this.onReceiveCall(response) ).catch( (error) => this.onError("call " + error ))}, (error) => {console.log ("setLocalDescription error:" + JSON.stringify(error)); });}, (error) => { alert("Create offer error:" + JSON.stringify(error));});} catch (e) {this.disconnect();alert("connect error: " + e);} }WebRtcStreamer.prototype.getIceCandidate = function() {fetch(this.srvurl + "/api/getIceCandidate?peerid=" + this.pc.peerid).then(this._handleHttpErrors).then( (response) => (response.json()) ).then( (response) => this.onReceiveCandidate(response)).catch( (error) => this.onError("getIceCandidate " + error ))}/** create RTCPeerConnection */WebRtcStreamer.prototype.createPeerConnection = function() {console.log("createPeerConnection config: " + JSON.stringify(this.pcConfig));this.pc = new RTCPeerConnection(this.pcConfig);var pc = this.pc;pc.peerid = Math.random(); pc.onicecandidate = (evt) => this.onIceCandidate(evt);pc.onaddstream = (evt) => this.onAddStream(evt);pc.oniceconnectionstatechange = (evt) => { console.log("oniceconnectionstatechange state: " + pc.iceConnectionState);if (this.videoElement) {if (pc.iceConnectionState === "connected") {this.videoElement.style.opacity = "1.0";} else if (pc.iceConnectionState === "disconnected") {this.videoElement.style.opacity = "0.25";} else if ( (pc.iceConnectionState === "failed") || (pc.iceConnectionState === "closed") ) {this.videoElement.style.opacity = "0.5";} else if (pc.iceConnectionState === "new") {this.getIceCandidate();}}}pc.ondatachannel = function(evt) { console.log("remote datachannel created:"+JSON.stringify(evt));evt.channel.onopen = function () {console.log("remote datachannel open");this.send("remote channel openned");}evt.channel.onmessage = function (event) {console.log("remote datachannel recv:"+JSON.stringify(event.data));}}pc.onicegatheringstatechange = function() {if (pc.iceGatheringState === "complete") {const recvs = pc.getReceivers();recvs.forEach((recv) => {if (recv.track && recv.track.kind === "video") {console.log("codecs:" + JSON.stringify(recv.getParameters().codecs))}});}}try {var dataChannel = pc.createDataChannel("ClientDataChannel");dataChannel.onopen = function() {console.log("local datachannel open");this.send("local channel openned");}dataChannel.onmessage = function(evt) {console.log("local datachannel recv:"+JSON.stringify(evt.data));}} catch (e) {console.log("Cannor create datachannel error: " + e);} console.log("Created RTCPeerConnnection with config: " + JSON.stringify(this.pcConfig) );return pc;}/** RTCPeerConnection IceCandidate callback*/WebRtcStreamer.prototype.onIceCandidate = function (event) {if (event.candidate) {if (this.pc.currentRemoteDescription) {this.addIceCandidate(this.pc.peerid, event.candidate); } else {this.earlyCandidates.push(event.candidate);}} else {console.log("End of candidates.");}}WebRtcStreamer.prototype.addIceCandidate = function(peerid, candidate) {fetch(this.srvurl + "/api/addIceCandidate?peerid="+peerid, { method: "POST", body: JSON.stringify(candidate) }).then(this._handleHttpErrors).then( (response) => (response.json()) ).then( (response) => {console.log("addIceCandidate ok:" + response)}).catch( (error) => this.onError("addIceCandidate " + error ))}/** RTCPeerConnection AddTrack callback*/WebRtcStreamer.prototype.onAddStream = function(event) {console.log("Remote track added:" + JSON.stringify(event));this.videoElement.srcObject = event.stream;var promise = this.videoElement.play();if (promise !== undefined) {promise.catch((error) => {console.warn("error:"+error);this.videoElement.setAttribute("controls", true);});}}/** AJAX /call callback*/WebRtcStreamer.prototype.onReceiveCall = function(dataJson) {console.log("offer: " + JSON.stringify(dataJson));var descr = new RTCSessionDescription(dataJson);this.pc.setRemoteDescription(descr).then(() => { console.log ("setRemoteDescription ok");while (this.earlyCandidates.length) {var candidate = this.earlyCandidates.shift();this.addIceCandidate(this.pc.peerid, candidate); }this.getIceCandidate()}, (error) => { console.log ("setRemoteDescription error:" + JSON.stringify(error)); });} /** AJAX /getIceCandidate callback*/WebRtcStreamer.prototype.onReceiveCandidate = function(dataJson) {console.log("candidate: " + JSON.stringify(dataJson));if (dataJson) {for (var i=0; i<dataJson.length; i++) {var candidate = new RTCIceCandidate(dataJson[i]);console.log("Adding ICE candidate :" + JSON.stringify(candidate) );this.pc.addIceCandidate(candidate).then( () => { console.log ("addIceCandidate OK"); }, (error) => { console.log ("addIceCandidate error:" + JSON.stringify(error)); } );}this.pc.addIceCandidate();}}/** AJAX callback for Error*/WebRtcStreamer.prototype.onError = function(status) {console.log("onError:" + status);}return WebRtcStreamer;})();if (typeof window !== 'undefined' && typeof window.document !== 'undefined') {window.WebRtcStreamer = WebRtcStreamer;}if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {module.exports = WebRtcStreamer;}