WebRTC初试用

编程入门 行业动态 更新时间:2024-10-10 03:33:06

WebRTC<a href=https://www.elefans.com/category/jswz/34/1762220.html style=初试用"/>

WebRTC初试用

1.WebRTC技术

在线视频传输,传统做法是做一个中继服务器,负责客户端的发现和数据的中介传输,那么就会产生一个很明显的问题,中继服务器需要传输大量的数据,不仅如此还有复杂的流信息控制以及同步等问题。而且,随着数据量的增大,中继服务器单机无法承载,不得不做负载均衡甚至地区分发等,大大增加系统复杂度,增加了各种成本,降低了稳定性。而且服务器作为中介,有记录用户传输数据的能力,用户的隐私问题也值得关注。所以,如果能够让客户机P2P的连接以及传输数据,让客户机自己去处理同步以及控制问题,自己去传输流数据,

这样即可大大减小系统的复杂度。WebRTC就是致力于建立统一的浏览器标准,来完成这种P2P的传输工作。

2.本文声明

由于WebRTC的大量功能还处于实验阶段,即使在MDN上面,很多接口也没有详细的介绍和说明,部分没有翻译,而网上的代码大多也过时

,因为WebRTC已经duplicate一部分函数了:例如RTCPeerConnection中createOffer函数的successCallback参数等。所以写

此文,大略的介绍一下RTC里面的部分基础组件和常用流程。另外由于这些API处于实验阶段,仍然可能变化,本文仅限写作时的时效性。

3.获取流

视频,音频是以流(stream)的形式进行网络传输,为了获取一个流,可以使用HTML的getUserMedia,由于目前支持该对象的浏览器 各不相同,暂时可以用下列代码获得:

getUserMedia = (navigator.getUserMedia ||navigator.webkitGetUserMedia ||navigator.mozGetUserMedia ||navigator.msGetUserMedia);

getUserMedia可以用来获取用户的视频/音频流,使用如下:

getUserMedia.call(navigator, {"audio": true,"video": true}, function(stream) {//绑定本地媒体流到video标签用于输出localVideoElement.src = URL.createObjectURL(stream);}, function(error) {//处理错误});

就像代码中所描述的,处理流的第二个参数中的匿名函数,将stream使用URL.createObjectURL创建一个blob的URL,这个URL可以

绑定到HTML5的Video标签播放(记得Video标签加上autoplay属性,不然就只有一张图了)。

4.信令传输

要实现Client到Client的直接传输,还需要服务器协调一些数据,比如最基本的,两个客户端的IP地址是什么,好让他们互相发现。另外

由于因特网的历史原因,NAT广泛用于全世界,所以,要实现P2PNAT穿透也是一个问题,NAT穿透的问题已在上一篇讲过,这盘文章在局域

网内做一个视频传输。和服务器的传输,到了这个时代,使用websocket有很多好处,不一一列举。websocket的基本使用如下:

//没有TLS协议的话用ws://,因为chrome等浏览器要求获取用户流的网站必须是安全的,所以一般都用了TLS(HTTPS)
var socket = new WebSocket('wss://0.0.0.0/xxx'); 
​
socket.onopen = function() { ... }
​
socket.onmessage = function(event) { //event.data是具体信息 }
​
socket.send(....);

5.客户端(浏览器)传输

浏览器间流的传输使用PeerConnection,这个对象封装了底层的传输,以及流数据的编码、同步控制,使用起来相当简易。同样,获取这个 对象也要兼容不同浏览器:

PeerConnection = (window.PeerConnection ||window.webkitPeerConnection00 ||window.webkitRTCPeerConnection ||window.mozRTCPeerConnection);

该对象的传输涉及几个概念,candidate是ICE候选信息,包括了对端的IP地址等信息,用于互相发现,offer和answer可能是用来同步

数据等等的,每次发送数据时,发送方都要发送一个offer过去,接收方收到后,根据offer更新自己的会话,接收方也可以发送answer

信令让发送方更新会话。发送方和接收方一开始就要确定,身份在整个传输中不变(确定谁是发送谁是接收就交给协调服务器好了)。同时,answer

信令在接收到offser之前是不能发送的,而且在发送offer信令的时候,也会发送candidate过去,所以,传输流程如下:

  1. 接收方准备好PeerConnection

  2. 发送方准备好PeerConnection,并在有流数据获取到的时候发送offer信令

  3. 当接收方收到offer信令,则更新本地会话,并开始在有流数据到达时发送answer信令

  4. 当发送方收到answer信令,更新本地会话

  5. 现在P2P通道已经建立

//准备PeerConnection
pc = new PeerConnection({"iceServers": []});
​
//收到ICE候选时发送ICE候选到其他客户端
pc.onicecandidate = function(event){socket.send(JSON.stringify({"type": "__ice_candidate","candidate": event.candidate}));
};
​
//当收到candidate信令(比如通过websocket)
pc.addIceCandidate(new RTCIceCandidate(data.candidate));
​
//当流数据到达时,接收方的处理(注意写法,回调函数的写法已经过时了):
pc.createAnswer().then(function(answer) {return pc.setLocalDescription(answer);
}).then(function() {socket.send(JSON.stringify({"type": "__answer","sdp": pc.localDescription}));
});
​
//当流数据到达时,发送方的处理(注意写法,回调函数的写法已经过时了):
pc.createOffer().then(function(offer) {return pc.setLocalDescription(offer);
}).then(function() {socket.send(JSON.stringify({"type": "__offer","sdp": pc.localDescription}));
});
​
//收到offer/answer的处理
pc.setRemoteDescription(new RTCSessionDescription(data.sdp));

注意:由于answer必须在收到offer之后才能发送方,所以接收方一开始不能设置流的处理函数(getUserMedia.call的第二个参数)

去发送offer,只有收到offer之后才去设置收到流后发送。

6.实例

由于我也是第一次试着使用WebRTC,所以以下代码也是为了大致说明流程,异常情况的处理和并发的处理都没有去做,仅作说明:

//HTML:
<!DOCTYPE HTML>
<html><head><title>开始裸聊</title></head><body><div id="queue">等待队列里现在有0人</div><span οnclick="start()" id="startB">加入聊天</span><div id="tip">请使用chrome/firefox浏览器</div><video autoplay id="remoteVideo"></video><video autoplay id="localVideo"></video></body><script>var socket = new WebSocket('wss://');var waitNum = 0;var joined = false;var remoteVideoElement = document.getElementById("remoteVideo");var localVideoElement = document.getElementById("localVideo");var getUserMedia, PeerConnection, pc;var isCaller = false;
​if (socket == undefined) {alert('你的浏览器太辣鸡了,我们不支持!');}socket.onopen = function(event) {socket.send('{"type":"ready"}');socket.onmessage = function(event) {var data = JSON.parse(event.data);if (data.type == "update") {var queue = document.getElementById("queue");queue.innerHTML = "等待队列里现在有" + data.num + "人";waitNum = data.num;}if (data.type == "start") {localStorage.remoteIp = data.remoteIp;isCaller = true;prepare();video();}/*if (data.type == "start2") {localStorage.remoteIp = data.remoteIp;isCaller = false;//setTimeout(function() {video()}, 1000);video();}*///如果是一个ICE的候选,则将其加入到PeerConnection中,否则设定对方的session描述为传递过来的描述if( data.type === "__ice_candidate" ){console.log(data);var mid = new RTCIceCandidate(data.candidate);pc.addIceCandidate(mid);}if (data.type == "__offer") {console.log(data);var mid = new RTCSessionDescription(data.sdp);prepare();pc.setRemoteDescription(mid);video();}if (data.type == "__answer") {console.log(data);var mid = new RTCSessionDescription(data.sdp);pc.setRemoteDescription(mid);}};socket.onclose = function(event) {console.log('Client notified socket has closed',event);};};
​function start() {if (joined) return;joined = true;var msg = {type: "join"};socket.send(JSON.stringify(msg));}
​function prepare() {getUserMedia = (navigator.getUserMedia ||navigator.webkitGetUserMedia ||navigator.mozGetUserMedia ||navigator.msGetUserMedia);PeerConnection = (window.PeerConnection ||window.webkitPeerConnection00 ||window.webkitRTCPeerConnection ||window.mozRTCPeerConnection);
​pc = new PeerConnection({"iceServers": []});//发送ICE候选到其他客户端pc.onicecandidate = function(event){socket.send(JSON.stringify({"type": "__ice_candidate","candidate": event.candidate}));};//如果检测到媒体流连接到本地,将其绑定到一个video标签上输出pc.onaddstream = function(event){remoteVideoElement.src = URL.createObjectURL(event.stream);};
​document.getElementById("startB").innerHTML = "";document.getElementById("tip").innerHTML = "";document.getElementById("queue").innerHTML = "";}
​function video() {//获取本地的媒体流,并绑定到一个video标签上输出,并且发送这个媒体流给其他客户端getUserMedia.call(navigator, {"audio": true,"video": true}, function(stream){//绑定本地媒体流到video标签用于输出localVideoElement.src = URL.createObjectURL(stream);//向PeerConnection中加入需要发送的流pc.addStream(stream);//如果是发送方则发送一个offer信令,否则发送一个answer信令if(isCaller){pc.createOffer().then(function(offer) {return pc.setLocalDescription(offer);}).then(function() {socket.send(JSON.stringify({"type": "__offer","sdp": pc.localDescription}));});} else {pc.createAnswer().then(function(answer) {return pc.setLocalDescription(answer);}).then(function() {socket.send(JSON.stringify({"type": "__answer","sdp": pc.localDescription}));});}}, function(error){//处理媒体流创建失败错误});
​}</script>
</html>
//服务器
package main
​
import ("encoding/json""io/ioutil""log""net/http"
​"github/gorilla/websocket"
)
​
type WS struct {Conn *websocket.ConnType int
}
​
var (upgrader   = websocket.Upgrader{CheckOrigin: func(r *http.Request) bool { return true }}waitQueue  = make([]string, 0)waitSocket = make(map[string]WS)pairSocket = make(map[string]string)
)
​
func HelloServer(w http.ResponseWriter, req *http.Request) {log.Println(req.RemoteAddr)data, err := ioutil.ReadFile("./wait.html")if err != nil {w.WriteHeader(404)return}w.Write(data)
}
​
func ChatHandle(w http.ResponseWriter, req *http.Request) {log.Println(req.RemoteAddr)data, err := ioutil.ReadFile("./1.html")if err != nil {w.WriteHeader(404)return}w.Write(data)
}
​
func BroadCast() {resp := make(map[string]interface{})for _, m := range waitSocket {resp["type"] = "update"resp["num"] = len(waitQueue)respMsg, _ := json.Marshal(resp)err := m.Conn.WriteMessage(m.Type, respMsg)if err != nil {log.Println("write:", err)}}
}
​
func WaitQueueHandle(w http.ResponseWriter, r *http.Request) {c, err := upgrader.Upgrade(w, r, nil)if err != nil {log.Print("upgrade:", err)return}log.Println(r.RemoteAddr, "Connet to server")defer func() {c.Close()delete(waitSocket, r.RemoteAddr)}()for {var resp = make(map[string]interface{})respMsg := []byte("{}")mt, message, err := c.ReadMessage()if err != nil {log.Println("read:", err)break}var jsonData map[string]interface{}err = json.Unmarshal(message, &jsonData)if err != nil {log.Println(err)continue}log.Printf("recv: %s", jsonData)typeMsg, ok := jsonData["type"].(string)if !ok {log.Println("type missing")continue}if typeMsg == "ready" {waitSocket[r.RemoteAddr] = WS{Conn: c,Type: mt,}resp["type"] = "update"resp["num"] = len(waitQueue)respMsg, _ = json.Marshal(resp)} else if typeMsg == "join" {if len(waitQueue) == 0 {waitQueue = append(waitQueue, r.RemoteAddr)BroadCast()continue} else {pair := waitQueue[0]waitQueue = append([]string{}, waitQueue[1:]...)BroadCast()resp["type"] = "start"resp["remoteIp"] = pairpairSocket[pair] = r.RemoteAddrpairSocket[r.RemoteAddr] = pairrespMsg, _ = json.Marshal(resp)c.WriteMessage(mt, respMsg)
​resp_t := make(map[string]interface{})resp_t["type"] = "start2"resp_t["remoteIp"] = pairrespMsg_t, _ := json.Marshal(resp_t)waitSocket[pair].Conn.WriteMessage(mt, respMsg_t)
​continue}} else if typeMsg == "__ice_candidate" || typeMsg == "__offer" || typeMsg == "__answer" {waitSocket[pairSocket[r.RemoteAddr]].Conn.WriteMessage(mt, message)continue}err = c.WriteMessage(mt, respMsg)if err != nil {log.Println("write:", err)break}}
}
​
func main() {http.HandleFunc("/", HelloServer)http.HandleFunc("/queue", WaitQueueHandle)http.HandleFunc("/chat", ChatHandle)err := http.ListenAndServeTLS(":443", "server.pem", "server.key", nil)if err != nil {log.Fatal("ListenAndServe: ", err)}
}

因为只是想试试,所以不要吐槽代码太垃圾了啦。

原文链接:WebRTC初试用-在线视频聊天室的基本流程_InsZVA的博客-CSDN博客

★文末名片可以免费领取音视频开发学习资料,内容包括(FFmpeg ,webRTC ,rtmp ,hls ,rtsp ,ffplay ,srs)以及音视频学习路线图等等。

见下方!↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓

 

更多推荐

WebRTC初试用

本文发布于:2024-02-13 07:42:23,感谢您对本站的认可!
本文链接:https://www.elefans.com/category/jswz/34/1757743.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文标签:初试   WebRTC

发布评论

评论列表 (有 0 条评论)
草根站长

>www.elefans.com

编程频道|电子爱好者 - 技术资讯及电子产品介绍!