初试用"/>
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过去,所以,传输流程如下:
-
接收方准备好PeerConnection
-
发送方准备好PeerConnection,并在有流数据获取到的时候发送offer信令
-
当接收方收到offer信令,则更新本地会话,并开始在有流数据到达时发送answer信令
-
当发送方收到answer信令,更新本地会话
-
现在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初试用
发布评论