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.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)以及音视频学习路线图等等。

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

猜你喜欢

转载自blog.csdn.net/yinshipin007/article/details/130015583