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.com/gorilla/websocket" ) type WS struct { Conn *websocket.Conn Type 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"] = pair pairSocket[pair] = r.RemoteAddr pairSocket[r.RemoteAddr] = pair respMsg, _ = json.Marshal(resp) c.WriteMessage(mt, respMsg) resp_t := make(map[string]interface{}) resp_t["type"] = "start2" resp_t["remoteIp"] = pair respMsg_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)以及音视频学习路线图等等。
见下方!↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓