WebRTC初试用-在线视频聊天室的基本流程

原创 2016年10月17日 17:39:09

WebRTC技术

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

本文声明

由于WebRTC的大量功能还处于实验阶段,即使在MDN上面,很多接口也没有详细的介绍和说明,部分没有翻译,而网上的代码大多也过时
,因为WebRTC已经duplicate一部分函数了:例如RTCPeerConnectioncreateOffer函数的successCallback参数等。所以写
此文,大略的介绍一下RTC里面的部分基础组件和常用流程。另外由于这些API处于实验阶段,仍然可能变化,本文仅限写作时的时效性。

获取流

视频,音频是以流(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属性,不然就只有一张图了)。

信令传输

要实现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(....);

客户端(浏览器)传输

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

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

该对象的传输涉及几个概念,candidate是ICE候选信息,包括了对端的IP地址等信息,用于互相发现,offeranswer可能是用来同步
数据等等的,每次发送数据时,发送方都要发送一个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之后才去设置收到流后发送。

实例

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

//HTML:
<!DOCTYPE HTML>
<html>
    <head>
        <title>开始裸聊</title>
    </head>
    <body>
        <div id="queue">等待队列里现在有0人</div>
        <span onclick="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+websocket多人视频通话

webRTc+ websocket实现多人视频通话,目前此demo只支持crome浏览器, 版本仅仅支持:ChromeStandalone_46.0.2490.80_Setup.1445829883 ...
  • heqinghua217
  • heqinghua217
  • 2016-08-10 17:50:32
  • 4366

C#WebSocket实时视频传输.zip

  • 2016年03月09日 11:44
  • 57KB
  • 下载

WebRTC VideoEngine超详细教程(一)——视频通话的基本流程

总述 在前一篇文章中,讲解了如何将OPENH264编解码器集成到WebRTC中,但是OPENH264只能编码baseline的H264视频,而且就编码质量而言,还是X264最好,本文就来讲解一下...
  • xyblog
  • xyblog
  • 2015-12-30 10:53:53
  • 1369

WEBRTC 视频接收原理及流程

创建解码器 VideoChannel::SetRemoteContent_w->BaseChannel::UpdateRemoteStreams_w-> WebRtcVideoChannel2::Ad...
  • doitsjz
  • doitsjz
  • 2016-07-25 12:39:56
  • 3419

最新webrtc视频全套教程

为了满足广大朋友需要,特地制作了webrtc视频教程,此课程是作者多年经验总结出的所制作的一套webrtc快速入门教程,学完此课程,你能搭建出一套android互通或者web互通或者android对w...
  • xyblog
  • xyblog
  • 2018-01-03 10:14:00
  • 899

WebRTC 之视频捕获——浏览器显示

什么是 WebRTC WebRTC(Web Real-Time Communication)是实现浏览器之间点对点实时通讯的一套技术规范(现在也支持 iOS 和 Android 应用)。20...
  • bytxl
  • bytxl
  • 2016-01-19 09:13:49
  • 1298

基于WebRTC的多人视频会议

Chinaunix首页 | 论坛 | 认证专区 | 博客 登录 | 注册      博文     博主    光阴过客hkyan.blog.chinaunix.net 人生就是一...
  • pxy1584417222
  • pxy1584417222
  • 2016-05-24 14:22:15
  • 2013

WebRTC源码中turnserver的使用方法

WebRTC的源码中自带了一个turnserver,介绍下用法
  • foruok
  • foruok
  • 2017-03-07 17:13:36
  • 2186

web即时通信1--WebSocket与WebRTC的三种实现方式对比

最近应项目组要求研究了下WebRTC(目前支持Firefox和Chrome),WebRTC,名称源自网页实时通信(Web Real-Time Communication)的缩写,是一个支持网页浏览器进...
  • jrn1012
  • jrn1012
  • 2014-12-17 16:01:01
  • 9993

nodejs搭建的<em>webrtc</em>视频<em>聊天室</em>

<em>webrtc</em> web视频<em>聊天室</em> nodejs搭建服务运行请查看README.md。... <em>webrtc</em> web视频<em>聊天室</em> nodejs搭建服务运行请查看...<em>WebSocket+</em>node.js创建即时通信的<em>Web聊天</em>服务器 ...
  • 2018年04月08日 00:00
收藏助手
不良信息举报
您举报文章:WebRTC初试用-在线视频聊天室的基本流程
举报原因:
原因补充:

(最多只允许输入30个字)