webRTC- LearningWebRTC-读书笔记

59 篇文章 0 订阅
50 篇文章 1 订阅

LearningWebRTC 目录

文章目录


前言

  • 适合HTMLJavaScript构建Web应用程序的有经验者
  • 打算利用用户间音频和视频交流力量来构建新的应用程序
  • 通过在用户之间转移高性能数据来实现应用程序
  • 本书是写给新入门的网络工程师
  • 实时通信的内部工作原理

前提

  • 掌握编程概念和网络基础
    sample:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Learning WebRTC - Chapter 4: Creating a RTCPeerCOnnection</title>
</head>
<body>
    <div id="container">
        <video id="yours" autoplay></video>
        <video id="theirs" autoplay></video>
    </div>

    <script src="main.js"></script>
</body>
</html>

推荐阅读


总结

  1. 获取用户媒体
  2. RTCPeerConnection, ICE, SDP, offer, answer
  3. STUN信令服务器
  4. 完整连接项目
  5. 传输数据
  6. 性能和安全

未来方向:

  • 点对点通信
  • 点对点传输数据
  • 服务器安全
  • 游戏服务器更新对点通信

1. 开启WebRTC之旅

本章基础知识:

  • 音频和视频领域的发展现状
  • WebRTC对音视频领域的影响
  • WebRTC主要特性及使用方法

在WebRTC平台传输音频和视频

需要考虑:
- 连接断开
- 数据丢失
- NAT穿透
API:
- 捕捉摄像头和麦克风
- 音视频解码
- 传输层
- 会话管理

  • 音频和视频的编解码:codec(多媒体数字信号编解码器)
  • 内置的编解码器:H.264, Opus, iSAC, VP8
  • WebRTC借鉴了其他传输层(AJAX, WebSockets)
  • 会话管理:通常称为信令,负责在浏览器中建立并管理多个连接

WebRTC的应用

  • 核心:在两个浏览器之间建立起来的一条点对点连接
  • 可以应用在:文件共享、文本聊天、多人游戏、货币流通
  • 连接低延迟、高性能,使用底层协议来提供高速性能,从而加速数据在网络间的流动,实现在短时间内传输大量的数据

2. 获取用户媒体

主要内容:

  1. 如何访问媒体设备
  2. 如何约束媒体流
  3. 如何处理多种设备
  4. 如何修改流数据

配置静态服务器

  1. npm install -g node-static
  2. static指定端口

媒体流页面

sample:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Learning WebRTC - Chapter 2: Get User Media</title>
</head>
<body>
    <video autoplay></video>
    <script src="main.js"></script>
</body>
</html>
// 函数
function hasUserMedia() {
    return !!(navigator.getUserMedia
            || navigator.webkitGetUserMedia
            || navigator.mozGetUserMedia
            || navigator.msGetUserMedia);
}

if(hasUserMedia()){
    navigator.getUserMedia = navigator.getUserMedia
                            || navigator.webkitGetUserMedia
                            || navigator.mozGetUserMedia
                            || navigator.msGetUserMedia;
    navigator.getUserMedia({
        video: true,
        audio: true
    }, stream => {
        let video = document.querySelector('video');
        try {
            video.src = window.URL.createObjectURL(stream);
        } catch(error){
            video.srcObject = stream;
        }
    }, err => {
        console.log(err);
    });
} else {
    alert("抱歉,你的浏览器不支持 getUserMedia");
}
// 类
class UserMedia {
    constructor() {
        this.hasUserMedia = !!(navigator.getUserMedia
            || navigator.webkitGetUserMedia
            || navigator.mozGetUserMedia
            || navigator.msGetUserMedia);
    }

    getMedia(tag){
        if(this.hasUserMedia){
            navigator.getUserMedia = navigator.getUserMedia
                || navigator.webkitGetUserMedia
                || navigator.mozGetUserMedia
                || navigator.msGetUserMedia;
            
            navigator.getUserMedia({
                video: true,
                audio: true
            }, stream => {
                let video = document.querySelector(tag);
                try {
                    video.src = window.URL.createObjectURL(stream);
                } catch(error){
                    video.srcObject = stream;
                }
            }, err => {                
                console.log(err);
                return err;
            });
        }else {
            alert("版本不支持");
            return;
        }
    }
}

let media = new UserMedia();
media.getMedia('video');
  • <video>通过window.URL.createObjectURL将流加载到该元素中
  • <video>不能接收JS作为参数,只能通过一些字符串来换取视频流
  • 函数在获取流对象后,会将它转换成一个本地的URL,这样标签就能从这个地址获取流数据

注意

  • <video>元素中应该包含一个autoplay属性,表示自动播放
  • 从摄像头获取stream对象并导入页面上的视频元素这个过程,如果用C/C++是非常繁琐的

媒体流的方法- getUserMedia API

API处理方法

navigator.getUserMedia({video: false, audio: true}, function (stream){
	// 视频流里不包含视频
})

限制视频捕捉

推荐阅读
  1. 最低分辨率
  2. 帧速率
  3. 视频宽高比
应用实例
  1. 长宽比sample:
function hasUserMedia() {
    return !!(navigator.getUserMedia
            || navigator.webkitGetUserMedia
            || navigator.mozGetUserMedia
            || navigator.msGetUserMedia);
}

if(hasUserMedia()){
    navigator.getUserMedia = navigator.getUserMedia
                            || navigator.webkitGetUserMedia
                            || navigator.mozGetUserMedia
                            || navigator.msGetUserMedia;
    navigator.getUserMedia({
        video: {
            mandatory: {
                minAspectRatio: 1.777,
                maxAspectRatio: 1.778,
                minWidth: 640,
                maxHeight: 480
            }
        },
        audio: false
    }, function (stream) {
        let video = document.querySelector('video');
        try {
            video.src = window.URL.createObjectURL(stream);
        } catch(error){
            video.srcObject = stream;
        }
    }, function (err) {
        console.log(err);
    });
} else {
    alert("抱歉,你的浏览器不支持 getUserMedia");
}
  1. 移动端使用
let constraints = {
    video: {
        mandatory: {
            minWidth: 640,
            minHeight: 480
        }
    },
    audio: false
};

if(/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|OperaMini/i
    .test(navigator.userAgent)){
        constraints = {
            video: {
                mandatory: {
                    minWidth: 480,
                    minHeight: 320,
                    maxWidth: 1024,
                    maxHeight: 768
                }
            },
            audio: false
        };
    }

navigator.getUserMedia(constraints, stream => {
    let video = document.querySelector('video');
    try{
        video.src = window.URL.createObjectURL(stream);
    }catch(error){
        video.srcObject = stream;
    }
}, err => {
    console.log(err);
});
  • 限制配置,决定着webRTC应用的性能

多设备处理

  • 设备上接驳多台摄像头和麦克风
  • 暴露了MediaSourceTrack的API
  • MediaStreamTrack.getSources已经弃用,现在用navigator.mediaDevices.enumerateDevices().then(function(sources))
navigator.mediaDevices.enumerateDevices().then(sources => {
    let audioSource = null;
    let videoSource = null;

    for(let i = 0; i < sources.length; ++i){
        let source = sources[i];
        if(source.kind === 'audio'){
            console.log("发现麦克风:", source.label, source.id);
            audioSource = source.id;
        } else if(source.kind === "video"){
            console.log("发现摄像头:", source.label, source.id);
            videoSource = source.id;
        } else {
            console.log("发现未知资源:", source);
        }
    }

    let constraints = {
        audio: {
            optional: [{sourceId: audioSource}]
        },
        video: {
            optional: [{sourceId: videoSource}]
        }
    };

    navigator.getUserMedia(constraints, stream => {
        let video = document.querySelector('video');
        try{
            video.src = window.URL.createObjectURL(stream);
        }catch(error){
            video.srcObject = stream;
        }
    },
        error => console.log(`出现错误${error}`)
    );
});

创建一个拍照

  • canvas可以绘制线条、图形和图片,可以制作游戏
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Learning WebRTC - Chapter 2: Get User Media</title>
    <style>

        video, canvas{
            border: 1px solid gray;
            width: 480px;
            height: 320px;
        }
    </style>
</head>
<body>
    <video autoplay></video>    
    <canvas></canvas>
    <button id="capture">Capture</button>
    <script src="photobooth.js"></script>
</body>
</html>
function hasUserMedia() {
    return !!(navigator.getUserMedia ||
            navigator.webkitGetUserMedia ||
            navigator.mozGetUserMedia ||
            navigator.msGetUserMedia);
}

if(hasUserMedia()){
    navigator.getUserMedia = navigator.getUserMedia
                        || navigator.webkitGetUserMedia
                        || navigator.mozGetUserMedia
                        || navigator.msContentScript;
    
    let video = document.querySelector('video');
    let canvas = document.querySelector('canvas');
    let streaming = false;

    navigator.getUserMedia({
        video: true,
        audio: false
    }, stream => {
        streaming = true;
        try{
            video.src = window.URL.createObjectURL(stream);
        }catch(error){
            video.srcObject = stream;
        }
    }, err => console.log(err)
    );

    document.querySelector('#capture').addEventListener('click',
        event => {
            if(streaming){
                canvas.width = video.clientWidth;
                canvas.height = video.clientHeight;
                
                let context = canvas.getContext('2d');
                context.drawImage(video, 0, 0);
            }
        }
    );
} else {
    alert("对不起,您的浏览器不支持");
}

修改媒体流

  • 图片滤镜
    添加css样式
.grayscale {
    -webkit-filter: grayscale(1);
    -moz-filter: grayscale(1);
    -ms-filter: grayscale(1);
    -o-filter: grayscale(1);
    filter: grayscale(1);
}

.sepia {
    -webkit-filter: sepia(1);
    -moz-filter: sepia(1);
    -ms-filter: sepia(1);
    -o-filter: sepia(1);
    filter: sepia(1);    
}

.invert {
    -webkit-filter: invert(1);
    -moz-filter: invert(1);
    -ms-filter: invert(1);
    -o-filter: invert(1);
    filter: invert(1);
}

js增加滤镜功能:

function hasUserMedia() {
    return !!(navigator.getUserMedia ||
            navigator.webkitGetUserMedia ||
            navigator.mozGetUserMedia ||
            navigator.msGetUserMedia);
}

if(hasUserMedia()){
    navigator.getUserMedia = navigator.getUserMedia
                        || navigator.webkitGetUserMedia
                        || navigator.mozGetUserMedia
                        || navigator.msContentScript;
    
    let video = document.querySelector('video');
    let canvas = document.querySelector('canvas');
    let streaming = false;

    navigator.getUserMedia({
        video: true,
        audio: false
    }, stream => {
        streaming = true;
        try{
            video.src = window.URL.createObjectURL(stream);
        }catch(error){
            video.srcObject = stream;
        }
    }, err => console.log(err)
    );

    document.querySelector('#capture').addEventListener('click',
        event => {
            if(streaming){
                canvas.width = video.clientWidth;
                canvas.height = video.clientHeight;
                
                let context = canvas.getContext('2d');
                context.drawImage(video, 0, 0);
            }
        }
    );

    let filters = ['', 'grayscale', 'sepia', 'invert'];
    let currentFilter = 0;

    document.querySelector('video').addEventListener('click', 
        event => {
            if(streaming){
                canvas.width = video.clientWidth;
                canvas.height = video.clientHeight;
                let context = canvas.getContext('2d');

                context.drawImage(video, 0, 0);
                currentFilter++;
                if(currentFilter > filters.length - 1)  currentFilter = 0;
                canvas.className = filters[currentFilter];
            }
        }
    );
} else {
    alert("对不起,您的浏览器不支持");
}
  • 图片添加文字
context.fillStyle = "white";
context.fillText("Hello World!", 10, 10);

3. 创建简单的WebRTC应用

  • 开发任何WebRTC应用的首个步骤就是创建RTCPeerConnection
  • 成功创建一个RTCPeerConnection的前提,就是需要理解浏览器创建对等连接的内部工作原理

本章内容

  1. 理解UDP传输协议和实时传输
  2. 在本地与其他用户发送信令和交涉
  3. 在Web上找到其他用户和NAT穿透
  4. 创建RTCPeerConnection

理解UDP传输协议和实时传输

实时传输要求双方间有快速的连接速度。

  • 典型的网络连接:需要将音频和视频放到同一帧中,并以40-60帧的速度发送给另一个用户
  • 因为允许数据丢失,人脑会对丢失的帧自动补成,所以UDP更适合,创建高性能应用
  • TCP不适合游戏中的流数据,游戏不需要可靠,只需要快
    UDP传输不保证的事情:
  1. 不保证数据发送或接收的先后顺序
  2. 不保证每一个数据包都能够传送到接收端
  3. 不跟踪每个数据包的状态

WebRTC API

主要技术:

  1. RTCPeerConnection对象
  2. 信号传递和交涉
  3. 会话描述协议-SDP
  4. 交互式连接建立-ICE

RTCPeerConnection对象

API的主入口

  • 初始化一个连接他人以及传送流媒体信息
  • 负责与另一个用户建立UDP连接
  • 功能:
    • 维护浏览器内会话和对等连接的状态
    • 对等连接建立

在这里插入图片描述

实例化对象:

let myConnection = new RTCPeerConnection(configuration);
myConnection.onaddstream = stream => console.log(stream);

信号传递和交涉

网络地址:IP地址和端口号组成
发送信令的过程:

  1. 对等连接创建潜在的候选列表
  2. 选择用户连接
  3. 信令层通知用户连接,是否接受或拒绝
  4. 连接接收后的通知
  5. 交换电脑硬件和软件信息
  6. 交换位置信息
  7. 连接成功或失败

会话描述协议-SDP

  • 用户需要传出信息指明视频解码器,何种网络。
  • SDP是基于字符串的二进制数据对象:<key>=<value>\n

sample:

let configuration = {
    bundlePolicy: "max-compat"
};
let myConnection = new RTCPeerConnection(configuration);
myConnection.onaddstream = function(stream) {
    console.log(stream);
};

清晰的路线到用户

  • 为了保证网络安全

使用的多种技术:

  • NAT会话穿透技术 - STUN
  • 中继技术穿透NAT - TURN
  • 交互式连接建立 - ICE

典型的WebRTC连接过程的架构:
在这里插入图片描述

  • 找到ip

STUN

  • 发请求给服务器,服务器再转发

使用STUN协议需要由一个支持STUN协议的服务器

  • 建立高质量的WebRTC应用实际上需要多个服务器,需要提供一套STUN和TURN服务器

TURN

  • 客户端在对等连接的双方之间增加一个转播
  • 资源消耗多
  • 需要从TURN服务器去下载、处理并重定向每一个用户发送过来的数据包

ICE

  • STUNTURN结合后的标准
  • 每一个ICE候选路径都是通过STUNTURN来找到的

WebRTC应用

  • 创建一个RTCPeerConnection
  • 创建DSP offer和回应
  • 为双方找到ICE候选路径
  • 创建一个成功的WebRTC连接

RTCPeerConnection

index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Learning WebRTC</title>
</head>
<body>
    <div id="container">
        <video id="yours" autoplay></video>
        <video id="theirs" autoplay></video>
    </div>
    <script src="./main.js"></script>
    <!-- <script src="main.js"></script> -->
</body>
</html>
main.js
  • hasUserMedia()
  • hasRTCPeerConnection():确保能够使用
function hasUserMedia() {
    navigator.getUserMedia = navigator.getUserMedia
                        || navigator.webkitGetUserMedia
                        || navigator.mozGetUserMedia
                        || navigator.msGetUserMedia;
    return !!navigator.getUserMedia;
}

function hasRTCPeerConnection() {
    window.RTCPeerConnection = window.RTCPeerConnection
                            || window.webkitRTCPeerConnection
                            || window.mozRTCPeerConnection;
    return !!window.RTCPeerConnection;
}
执行状态流程图

在这里插入图片描述

  1. 从用户得到媒体流
  2. 建立对等连接
  3. 一方创建offer,另一方做准备。offer和返回都是发送信令过程中的一部分
  4. 找到合适的端口和IP组合进行链接,成功后开始共享信息
捕获用户的摄像头
let yourVideo = document.querySelector("#yours");
let theirVideo = document.querySelector("#theirs");
let yourConnection, theirConnection;

if(hasUserMedia()){
    navigator.getUserMedia({
        video: true,
        audio: false
    }, stream => {
        
            try{
                yourVideo.src = window.URL.createObjectURL(stream);
            }catch(error){
                yourVideo.srcObject = stream;
            }


        if(hasRTCPeerConnection()){
            startPeerConnection(stream);
        } else {
            alert("你的浏览器不支持webRTC");
        }
    }, error => {
        alert("你的浏览器不支持WebRtc");
    });
}
RTCPeerConnection对象
  • 建立SCP offer和返回,为双方寻找ICE候选路径
function startPeerConnection(stream) {
    let configuration = {
        {
        	"iceServers": [{"url": "stun:127.0.0.1:9876"}]
    	}
    };

	yourConnection = new webkitRTCPeerConnection(configuration);
	theirConnection = new webkitRTCPeerConnection(configuration);
};
建立SDP Offer和返回
  • 执行offer和返回answer这个过程以构成对等连接
function startPeerConnection(stream) {
    let configuration = {
        "iceServers": [
            {"url": "stun:127.0.0.1:9876"}
        ]
    };

    yourConnection = new webkitRTCPeerConnection(configuration);
    theirConnection = new webkitRTCPeerConnection(configuration);

    // 开始offer
    yourConnection.createOffer(offer => {
        yourConnection.setLocalDescription(offer);
        theirConnection.setRemoteDescription(offer);

        theirConnection.createAnswer(offer => {
            theirConnection.setLocalDescription(offer);
            yourConnection.setRemoteDescription(offer);
        });
    });    
}
  • 由于通信双方在同一个浏览器窗口中,确保用户收到offer时不用执行多次异步操作,实现offer/answer机制
寻找ICE候选路径
  • 连接用户不在同一个浏览器中时,将需要一个服务器。由于要跨多个浏览器窗口执行,将发生许多同步操作,会导致环境不稳定
  • 建立对等连接的最后一部分是,在双方间传递ICE候选路径,以便相互连接
function startPeerConnection(stream) {
    let configuration = {
        "iceServers": [
            {"url": "stun:127.0.0.1:9876"}
        ]
    };

    yourConnection = new webkitRTCPeerConnection(configuration);
    theirConnection = new webkitRTCPeerConnection(configuration);

    // 创建ICE处理
    yourConnection.onicecandidate = event => {
        if(event.candidate){
            theirConnection.addIceCandidate(new RTCIceCandidate(event.candidate));
        }
    };

    theirConnection.onicecandidate = event => {
        if(event.candidate){
            yourConnection.addIceCandidate(new RTCIceCandidate(event.candidate));
        }
    };

    // 开始offer
    yourConnection.createOffer(offer => {
        yourConnection.setLocalDescription(offer);
        theirConnection.setRemoteDescription(offer);

        theirConnection.createAnswer(offer => {
            theirConnection.setLocalDescription(offer);
            yourConnection.setRemoteDescription(offer);
        });
    });
}
  • 全部是事件驱动
  • 寻找ICE候选路径是异步的
  • 浏览器会不停地搜寻,直到尽可能多的创建良好且稳定的对等连接的候选路径
  • 当我们从theirConnection中获取ICE候选路径时,需要将路径加入到yourConnections中。当另一方跟我们不在同一个网络时,这些数据会横跨整个互联网
加入流和打磨
  • 调用onaddstream来通知用户,流已经被加入
// 监听流的创建
yourConnection.addStream(stream);
theirConnection.onaddstream = function(e) {
    theirVideo.src = window.URL.createObjectURL(e.stream);
};

//修改后
stream.getTracks().forEach(track => {
    yourConnection.addTrack(track, stream);
    theirConnection.onTrack = event => {
        try{
            theirVideo.src = window.URL.createObjectURL(event.stream);
        }catch(error){
            theirVideo.srcObject = event.stream;
        }

    };
});
增加css样式
body {
    background-color: #3D6DF2;
    margin-top: 15px;
}

video {
    background: black;
    border: 1px solid gray;
}

#container {
    position: relative;
    display: block;
    margin: 0 auto;
    width: 500px;
    height: 500px;    
}

#yours {
    width: 150px;
    height: 150px;
    position: absolute;
    top: 15px;
    right: 15px;
}

#theirs {
    width: 500px;
    height: 500px;
}body {
    background-color: #3D6DF2;
    margin-top: 15px;
}

video {
    background: black;
    border: 1px solid gray;
}

#container {
    position: relative;
    display: block;
    margin: 0 auto;
    width: 500px;
    height: 500px;    
}

#yours {
    width: 150px;
    height: 150px;
    position: absolute;
    top: 15px;
    right: 15px;
}

#theirs {
    width: 500px;
    height: 500px;
}

在这里插入图片描述

代码总结

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Learning WebRTC</title>
    <link rel="stylesheet" href="./style.css" type="text/css">
</head>
<body>
    <div id="container">
        <video id="yours" autoplay></video>
        <video id="theirs" autoplay></video>
    </div>
    <script src="./main.js"></script>
    <!-- <script src="main.js"></script> -->
</body>
</html>

style.css

body {
    background-color: #3D6DF2;
    margin-top: 15px;
}

video {
    background: black;
    border: 1px solid gray;
}

#container {
    position: relative;
    display: block;
    margin: 0 auto;
    width: 500px;
    height: 500px;    
}

#yours {
    width: 150px;
    height: 150px;
    position: absolute;
    top: 15px;
    right: 15px;
}

#theirs {
    width: 500px;
    height: 500px;
}

main.js

function hasUserMedia() {
    navigator.getUserMedia = navigator.getUserMedia
                        || navigator.webkitGetUserMedia
                        || navigator.mozGetUserMedia
                        || navigator.msGetUserMedia;
    return !!navigator.getUserMedia;
}

function hasRTCPeerConnection() {
    window.RTCPeerConnection = window.RTCPeerConnection
                            || window.webkitRTCPeerConnection
                            || window.mozRTCPeerConnection;
    return !!window.RTCPeerConnection;
}

function startPeerConnection(stream) {
    let configuration = {
        "iceServers": [
            {"url": "stun:127.0.0.1:9876"}
        ]
    };

    yourConnection = new webkitRTCPeerConnection(configuration);
    theirConnection = new webkitRTCPeerConnection(configuration);

    // 创建ICE处理
    yourConnection.onicecandidate = event => {
        if(event.candidate){
            theirConnection.addIceCandidate(new RTCIceCandidate(event.candidate));
        }
    };

    theirConnection.onicecandidate = event => {
        if(event.candidate){
            yourConnection.addIceCandidate(new RTCIceCandidate(event.candidate));
        }
    };

    // 开始offer
    yourConnection.createOffer(offer => {
        yourConnection.setLocalDescription(offer);
        theirConnection.setRemoteDescription(offer);

        theirConnection.createAnswer(offer => {
            theirConnection.setLocalDescription(offer);
            yourConnection.setRemoteDescription(offer);
        });
    });

    // 监听流创建
    stream.getTracks().forEach(track => {
        yourConnection.addTrack(track, stream);
        theirConnection.onTrack = event => {
            try{
                theirVideo.src = window.URL.createObjectURL(event.stream);
            }catch(error){
                theirVideo.srcObject = event.stream;
            }

        };
    });
    
}

let yourVideo = document.querySelector("#yours");
let theirVideo = document.querySelector("#theirs");
let yourConnection, theirConnection;

if(hasUserMedia()){
    navigator.getUserMedia({
        video: true,
        audio: false
    }, stream => {
        
            try{
                yourVideo.src = window.URL.createObjectURL(stream);
            }catch(error){
                yourVideo.srcObject = stream;
            }


        if(hasRTCPeerConnection()){
            startPeerConnection(stream);
        } else {
            alert("你的浏览器不支持webRTC");
        }
    }, error => {
        alert("你的浏览器不支持WebRtc");
    });
}

4. 创建信令服务器

  • 创建完整的WebRTC应用,需要抛开客户端的开发,转而为服务端的开发

涵盖内容

  1. Node.js
  2. WebSocket
  3. 识别用户
  4. 发起和应答WebRTC通话
  5. 处理ICE候选路径的传送
  6. 挂断通话

构建信令服务器

  • 将不在同一个电脑中的两个用户连接起来
  • 服务器的目的是通过网络传输代替原先的信令机制
  • 对多个用户做出回应:
    • 允许一方用户呼叫另一方从而在双方间建立WebRTC连接
    • 一旦用户呼叫了另一方,服务器将会在双方间传递请求,应答和ICE候选路径

在这里插入图片描述

流程:

  • 服务器建立连接时的信息流
  • 登陆服务器开始,登录向服务器端发送一个字符串形式的用户标识,确保没有被使用
  • 登录进入,开始呼叫,通过使用对方的标识码发送请求
  • 发送离开信息来终止连接
  • 此流程主要用来作为互相发送信息的通道

注意:

  • 由于发送信令的实现没有任何规则,可以使用任意协议、技术或者模式

WebSockets

  • 建立WebRTC连接所需的步骤必须是实时的,最好使用WebSockets,不能使用WebRTC对等连接实时传递消息

  • Socket以字符串和二进制码方式双向发送信息

  • 完全依赖于WebSocket框架:Meteor JavaScript framework

  • npm安装websocketnpm install ws

  • wscatnpm install wscat

server.js

const WebSocketServer = require('ws').Server,
    wss = new WebSocketServer({port: 8888});

wss.on("connection", connection => {
    console.log("User connected");

    connection.on("message", message => {
        console.log("Got message:", message);
    });

    connection.send("hello world!")
});
  • 监听服务器端的connection事件,当用户与服务器建立websocket连接时,会调用此,并有连接方的所有信息

  • 安装wscat进行测试:npm install -g ws, wscat -c ws://localhost:8888,或者前端测试

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <script>
        let websocket = new WebSocket("ws://localhost:8888");

    </script>
</body>
</html>

识别用户

  • 典型的网络应用中,服务器需要一种方法来识别连接的用户
  • 遵循唯一规则,让每一个用户有一个字符串形式的标识,即用户名

仅需一个id来标识

const WebSocketServer = require('ws').Server,
    wss = new WebSocketServer({port: 8888});

wss.on("connection", connection => {
    console.log("User connected");

    connection.on("message", message => {
        // console.log("Got message:", message);
        let data;

        try{
            data = JSON.parse(message);
        }catch(e) {
            console.log(e);
            data = {};
        }
    });

    connection.send("hello world!")
});
  • 由于websocket只允许字符和二进制数据,用JSON格式的结构化消息

存储对象

  • 用哈希图存储数据,在JS中叫对象
const WebSocketServer = require('ws').Server,
    wss = new WebSocketServer({port: 8888}),
      users = {};
wss.on("connection", connection => {
    console.log("User connected");

    connection.on("message", message => {
        // console.log("Got message:", message);
        let data;

        try{
            data = JSON.parse(message);
        }catch(e) {
            console.log(e);
            data = {};
        }
    });

    connection.send("hello world!")
});

login登录

  • 用户发送login类型信息才能登录
  • 客户端发送每一个信息增加type字段

服务器端:

const WebSocketServer = require('ws').Server,
    wss = new WebSocketServer({port: 8888}),
    users = {};

wss.on("connection", connection => {
    console.log("User connected");

    connection.on("message", message => {
        // console.log("Got message:", message);
        let data;

        try{
            data = JSON.parse(message);
        }catch(e) {
            console.log(e);
            data = {};
        }


        switch(data.type) {
            case "login":
                console.log("User logged in as", data.name);
                if(users[data.name]) {
                    sendTo(connection, {
                        type: "login",
                        success: false
                    });
                }else {
                    users[data.name] = connection;
                    connection.name = data.name;
                    sendTo(connection, {
                        type: "login",
                        success: true
                    });
                }
                break;
                
            default:
                sendTo(connection, {
                    type: "error",
                    message: "Unrecognized command: " + data.type;
                });

                break;
        }
    });

    connection.send("hello world!")
});

function sendTo(conn, message) {
    conn.send(JSON.stringify(message));
}
  • 如果有用户登录ID,就拒绝

断开收尾:

const WebSocketServer = require('ws').Server,
    wss = new WebSocketServer({port: 8888}),
    users = {};

wss.on("connection", connection => {
    console.log("User connected");

    connection.on("message", message => {
        // console.log("Got message:", message);
        let data;

        try{
            data = JSON.parse(message);
        }catch(e) {
            console.log(e);
            data = {};
        }


        switch(data.type) {
            case "login":
                console.log("User logged in as", data.name);
                if(users[data.name]) {
                    sendTo(connection, {
                        type: "login",
                        success: false
                    });
                }else {
                    users[data.name] = connection;
                    connection.name = data.name;
                    sendTo(connection, {
                        type: "login",
                        success: true
                    });
                }
                break;
                
            default:
                sendTo(connection, {
                    type: "error",
                    message: "Unrecognized command: " + data.type
                });

                break;
        }
    });

});

wss.on("close", function(){
    if(connection.name){
        delete users[connection.name];
    }
});

function sendTo(conn, message) {
    conn.send(JSON.stringify(message));
}

测试数据:{"type": "login", "name": "foo"}

发起通话

  • 创建offer处理器,用户用来呼叫另一方

  • 呼叫初始化的过程和WebRTCoffer分开最好,但此处结合

case "offer":
    console.log("sending offer to:", data.name);
    let conn = users[data.name];

    if(conn != null){
        connection.otherName = data.name;
        sendTo(conn, {
            type: "offer",
            offer: data.offer,
            name: connection.name
        });
    }
    break;

讲解:

  1. 首先获取试图呼叫用户的connection对象
  2. 检查另一用户是否在服务器上,若存在则发送offer
  3. 此方法使用于任何双方间的呼叫技术
  4. 缺少错误失败处理

呼叫应答

  • 应答:服务器仅将消息作为answer传递给另一方
case "answer":
    console.log("sending answer to:", data.name);
    let conn = users[data.name];

    if(conn != null){
        connection.otherName = data.name;
        sendTo(conn, {
            type: "answer",
            answer: data.answer
        })
    }
    break;
  • 如果一个用户先发送answer而非offer,将会扰乱服务器的实现
  • 但实现了webRTCRTCPeerConnectioncreateOffercreateAnswer

测试:

# 1
{"type": "login", "name": "UserA"}
# 2
{"type": "login", "name": "UserB"}
# 1
{"type": "offer", "name": "UserB", "offer": "hello"}
# 2
{"type": "answer", "name": "UserA", "answer": "hello to you too!"}

在这里插入图片描述

处理ICE候选路径

  • WebRTC信令的最后一部分是在用户间处理ICE候选路径
  • 使用之前的技术在用户间传递消息。但此类消息的不同在于,每一个用户可能都需要发送多次,且在双方用户间会以任何顺序发送

添加candidate处理器:

case "candidate":
    console.log("sending to", data.name);
    conn = users[data.name];
    if(conn != null){
        sendTo(conn, {
            type: "candidate",
            candidate: data.candidate
        });
    }
    break;
  • 由于通信已经建立,不需要在函数里添加另一方用户的名字类似于offer, answer,在双方间传递消息
# 1
{"type": "login", "name": "UserA"}
# 2
{"type": "login", "name": "UserB"}
# 1
{"type": "offer", "name": "UserB", "offer": "hello"}
# 2
{"type": "answer", "name": "UserA", "answer": "hello to you too!"}
# 1
{"type": "candidate", "name": "userA", "candidate": "test"}

呼叫挂断

  • 用户从另一方断开,从而可以呼叫其他用户
  • 通知服务器断开用户引用
case "leave":
    console.log("Disconnected user from ", data.name);
    conn = users[data.name];
    conn.otherName = null;

    if(conn != null){
        sendTo(conn, {
            type: "leave"
        });
    }
    break;
  • 这段代码也会通知另一个用户leave事件的触发,这样可以对等地断开对等连接
  • 当用户从信令服务器掉线时,我们也需要做响应的处理,不再提供服务,结束通话
wss.on("close", function(){
    if(connection.name){
        delete users[connection.name];

        if(connection.otherName) {
            console.log("Disconnected,",connection.otherName);
            let conn = users[connection.otherName];
            conn.otherName = null;

            if(conn != null){
                sendTo(conn,{
                    type: "leave"
                });
            }
        }
    }
});

完整的信令服务器

const WebSocketServer = require('ws').Server,
    wss = new WebSocketServer({port: 8888}),
    users = {};

wss.on("connection", connection => {
    console.log("User connected");

    connection.on("message", message => {
        // console.log("Got message:", message);
        let data, conn;

        try{
            data = JSON.parse(message);
        }catch(e) {
            console.log(e);
            data = {};
        }


        switch(data.type) {
            case "login":
                console.log("User logged in as", data.name);
                if(users[data.name]) {
                    sendTo(connection, {
                        type: "login",
                        success: false
                    });
                }else {
                    users[data.name] = connection;
                    connection.name = data.name;
                    sendTo(connection, {
                        type: "login",
                        success: true
                    });
                }
                break;
            
            case "offer":
                console.log("sending offer to:", data.name);
                conn = users[data.name];

                if(conn != null){
                    connection.otherName = data.name;
                    sendTo(conn, {
                        type: "offer",
                        offer: data.offer,
                        name: connection.name
                    });
                }
                break;

            case "answer":
                console.log("sending answer to:", data.name);
                conn = users[data.name];

                if(conn != null){
                    connection.otherName = data.name;
                    sendTo(conn, {
                        type: "answer",
                        answer: data.answer
                    })
                }
                break;

            case "candidate":
                console.log("sending to", data.name);
                conn = users[data.name];

                if(conn != null){
                    sendTo(conn, {
                        type: "candidate",
                        candidate: data.candidate
                    });
                }
                break;
            
            case "leave":
                console.log("Disconnected user from ", data.name);
                conn = users[data.name];
                conn.otherName = null;

                if(conn != null){
                    sendTo(conn, {
                        type: "leave"
                    });
                }
                break;
                
            default:
                sendTo(connection, {
                    type: "error",
                    message: "Unrecognized command: " + data.type
                });

                break;
        }
    });

});

wss.on("close", function(){
    if(connection.name){
        delete users[connection.name];

        if(connection.otherName) {
            console.log("Disconnected,",connection.otherName);
            let conn = users[connection.otherName];
            conn.otherName = null;

            if(conn != null){
                sendTo(conn,{
                    type: "leave"
                });
            }
        }
    }
});

wss.on("listening", () => {
    console.log("Server started...");
});

function sendTo(conn, message) {
    conn.send(JSON.stringify(message));
}
  • websocket支持SSL,可以启用wss://

websocket困境

  • 防火墙问题
  • 在代理设置下非常不稳定
  • webrtc延时导致消息处理混乱

代替技术 - XMPP, SIP

  • XMPP
  • SIP

5. 连接客户端

涵盖内容

  • 从客户端获取到服务器的连接
  • 识别各个连接端的用户
  • 两个远程用户发起通话
  • 结束通话

连接

  • 包含两个页面:输入用户名,呼叫其他用户

创建页面

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<style>
    body{
        background-color: #3D6DF2;
        margin-top: 15px;
        font-family: sans-serif;
        color: white;
    }

    video {
        background: black;
        border: 1px solid gray;
    }

    .page {
        position: relative;
        display: block;
        margin: 0 auto;
        width: 500px;
        height: 500px;
    }

    #yours{
        width: 150px;
        height: 150px;
        position: absolute;
        top: 15px;
        right: 15px;
    }

    #theirs {
        width: 500px;
        height: 500px;
    }
</style>
<body>
    <div id="login-page" class="page">
        <h2>Login As</h2>
        <input type="text" id="username">
        <button id="login">Login</button>
    </div>

    <div id="call-page" class="page">
        <video id="yours" autoplay></video>
        <video id="theirs" autoplay></video>

        <input type="text" id="their-username">
        <button id="call">Call</button>
        <button id="hang-up">Hang Up</button>
    </div>
</body>
<script src="client.js"></script>
</html>

获取一个连接

  • 与信令服务器创建连接
let name,
    connectedUser;

let connection = new WebSocket('ws://localhost:8888');

connection.onopen = function() {
    console.log("Connected");
};

connection.onmessage = function(message){
    console.log("Got message", message.data);

    let data = JSON.parse(message.data);

    switch(data.type){
        case "login":
            onLogin(data.success);
            break;
        case "offer":
            onOffer(data.offer, data.name);
            break;
        case "answer":
            onAnswer(data.answer);
            break;
        case "candidate":
            onCandidate(data.candidate);
            break;
        case "leave":
            onLeave();
            break;
        default:
            break;
    }
};

connection.onerror = function(err) {
    console.log(err);
}

function send(message) {
    if(connectedUser) {
        message.name = connectedUser;
    }

    connection.send(JSON.stringify(message));
}

登录

let loginPage = document.querySelector('#login-page'),
    usernameInput = document.querySelector('#username'),
    loginButton = document.querySelector('#login'),
    callPage = document.querySelector('#call-page'),
    theirUsernameInput = document.querySelector('#theirusername'),
    callButton = document.querySelector('#call'),
    hangUpButton = document.querySelector('#hang-up');

callPage.style.display = 'none';

loginButton.addEventListener('click', event=>{
    name = usernameInput.value;

    if(name.length > 0){
        send({
            type: "login",
            name: name
        });
    }
});

function onLogin(success) {
    if(success === false){
        alert("Login unsuccessfully, please try a different name.");
    }else {
        loginPage.style.display = 'none';
        callPage.style.display = 'block';
    }

    // 准备好通话的通道
    startConnection();

}

开始对等连接

  1. 从相机中获取视频流
  2. 验证用户的浏览器是否支持WebRTC
  3. 创建RTCPeerConnection对象
let yourVideo = document.querySelector('#yours'),
    theirVideo = document.querySelector('#theirs'),
    yourConnection,
    connectedUser,
    stream;

function startConnection() {
    if(hasUserMedia()){
        navigator.getUserMedia({
            video: true,
            audio: true
        },
        myStream => {
            stream = myStream;
            
            try{
                yourVideo.src = window.URL.createObjectURL(stream);
            }catch(e){
                yourVideo.srcObject = stream;
            }

            if(hasRTCPeerConnection()){
                setupPeerConnection(stream);
            }else{
                alert("不支持webRTC");
            }
        },
        error => {
            console.log(error);
        }
        );
    }else{
        alert("不支持webRTC");
    }
}

function setupPeerConnection(stream) {
    let configuration = {
        "iceServers":[
            {"url":"stun:localhost:8888"}
        ]
    };

    yourConnection = new RTCPeerConnection(configuration);

    // 设置流量监听
    yourConnection.addStream(stream);
    yourConnection.onaddstream = function(e) {
        try{
            theirVideo.src = window.URL.createObjectURL(e.stream);
        }catch(e){
            theirVideo.srcObject = stream;
        }
    };

    yourConnection.onicecandidate = function(event) {
        if(event.candidate){
            send({
                type: "candidate",
                candidate: event.candidate
            });
        }
    };
}

function hasUserMedia() {
    navigator.getUserMedia = navigator.getUserMedia
                        || navigator.webkitGetUserMedia
                        || navigator.mozGetUserMedia
                        || navigator.msGetUserMedia;
    return !!navigator.getUserMedia;
}

function hasRTCPeerConnection() {
    window.RTCPeerConnection = window.RTCPeerConnection
                            || window.webkitRTCPeerConnection
                            || window.mozRTCPeerConnection;

    window.RTCSessionDescription = window.RTCSessionDescription
                            || window.webkitRTCSessionDescription
                            || window.mozRTCSessionDescription;
    
    window.RTCIceCandidate = window.RTCIceCandidate
                            || window.webkitRTCIceCandidate
                            || window.mozeRTCIceCandidate;

    return !!window.RTCPeerConnection;
}

发起通话

  • setLocalDescription():更改与连接关联的本地描述。此说明指定连接的本地端的属性,包括媒体格式。
  • setRemoteDescription():将指定的会话描述设置为远程对等方的当前提供或应答。描述指定连接远端的属性,包括媒体格式。
  • addIceCandidate():通过信令信道从远程对等方接收新的ICE候选,它通过调用将新接收的候选发送到浏览器的ICE代理
callButton.addEventListener('click', function(){
    let theirUsername = theirUsernameInput.value;

    if(theirUsername.length > 0){
        startPeerConnection(theirUsername);
    }
});

function startPeerConnection(user) {
    connectedUser = user;

    // offer
    yourConnection.createOffer(offer=>{
        send({
            type: "offer",
            offer: offer
        });
        yourConnection.setLocalDescription(offer);
    },
    err => {
        alert("An error has occurred");
    });
}

function onOffer(offer, name) {
    connectedUser = name;
    yourConnection.setRemoteDescription(new RTCSessionDescription(offer));

    yourConnection.createAnswer(function(answer){
        yourConnection.setLocalDescription(answer);

        send({
            type: "answer",
            answer: answer
        });
    },
    err =>{
        alert("An error");
    }
    );
}

function onAnswer(answer){
    yourConnection.setRemoteDescription(new RTCSessionDescription(answer));
}

function onCandidate(candidate){
    yourConnection.addIceCandidate(new RTCIceCandidate(candidate));
}

检测通信

  • 调试实时应用困难在:许多事件发生在同一时刻,要完整描述某一时刻发生了什么很难

  • ChromeView->Developer->Developer Tools可以看到webSocket得通信状态

挂断电话

  • 通知其他用户关闭通话
  • 销毁本地连接,允许进行新的通话

过程:

  1. 通知服务器,断开连接
  2. 通知RTCPeerConnection关闭,停止发送数据流给其他用户
  3. 再次设置,连接实例设置为打开状态,以接受新的通话
hangUpButton.addEventListener("click", ()=>{
    send({
        type: "leave"
    });

    onLeave();
});

function onLeave() {
    connectedUser = null;
    theirVideo.srcObject = null;
    yourConnection.close();
    yourConnection.onicecandidate = null;
    yourConnection.onaddstream = null;
    setupPeerConnection(stream);
}

总结

  • 全部代码整合
// 变量声明
let name,
    connectedUser;

let connection = new WebSocket('ws://localhost:8888');

let yourVideo = document.querySelector('#yours'),
    theirVideo = document.querySelector('#theirs'),
    yourConnection,
    stream;

let loginPage = document.querySelector('#login-page'),
    usernameInput = document.querySelector('#username'),
    loginButton = document.querySelector('#login'),
    callPage = document.querySelector('#call-page'),
    theirUsernameInput = document.querySelector('#their-username'),
    callButton = document.querySelector('#call'),
    hangUpButton = document.querySelector('#hang-up');

callPage.style.display = 'none';

// 点击按钮登录
loginButton.addEventListener('click', event=>{
    name = usernameInput.value;

    if(name.length > 0){
        send({
            type: "login",
            name: name
        });
    }
});

// websocket 连接
connection.onopen = function() {
    console.log("Connected");
};

// 监听websocket信息
connection.onmessage = function(message){
    console.log("Got message", message.data);

    let data = JSON.parse(message.data);

    switch(data.type){
        case "login":
            onLogin(data.success);
            break;
        case "offer":
            onOffer(data.offer, data.name);
            break;
        case "answer":
            onAnswer(data.answer);
            break;
        case "candidate":
            onCandidate(data.candidate);
            break;
        case "leave":
            onLeave();
            break;
        default:
            break;
    }
};

// websocket报错信息
connection.onerror = function(err) {
    console.log(err);
}

// Alia 以JSON格式发送信息
function send(message) {
    if(connectedUser) {
        message.name = connectedUser;
    }

    connection.send(JSON.stringify(message));
}

function onLogin(success) {
    if(success === false){
        alert("Login unsuccessfully, please try a different name.");
    }else {
        loginPage.style.display = 'none';
        callPage.style.display = 'block';
    }

    // 准备好通话的通道
    startConnection();
}

// call呼叫
callButton.addEventListener('click', function(){
    let theirUsername = theirUsernameInput.value;

    if(theirUsername.length > 0){
        startPeerConnection(theirUsername);
    }
});

// 挂断
hangUpButton.addEventListener("click", ()=>{
    send({
        type: "leave"
    });

    onLeave();
});

function onOffer(offer, name) {
    connectedUser = name;
    yourConnection.setRemoteDescription(new RTCSessionDescription(offer));

    yourConnection.createAnswer(function(answer){
        yourConnection.setLocalDescription(answer);

        send({
            type: "answer",
            answer: answer
        });
    },
    err =>{
        alert("An error");
    }
    );
}

function onAnswer(answer){
    yourConnection.setRemoteDescription(new RTCSessionDescription(answer));
}

function onCandidate(candidate){
    yourConnection.addIceCandidate(new RTCIceCandidate(candidate));
}


function onLeave() {
    connectedUser = null;
    theirVideo.srcObject = null;
    yourConnection.close();
    yourConnection.onicecandidate = null;
    yourConnection.onaddstream = null;
    setupPeerConnection(stream);
}


// 函数的polyfill
function hasUserMedia() {
    navigator.getUserMedia = navigator.getUserMedia
                        || navigator.webkitGetUserMedia
                        || navigator.mozGetUserMedia
                        || navigator.msGetUserMedia;
    return !!navigator.getUserMedia;
}

function hasRTCPeerConnection() {
    window.RTCPeerConnection = window.RTCPeerConnection
                            || window.webkitRTCPeerConnection
                            || window.mozRTCPeerConnection;

    window.RTCSessionDescription = window.RTCSessionDescription
                            || window.webkitRTCSessionDescription
                            || window.mozRTCSessionDescription;
    
    window.RTCIceCandidate = window.RTCIceCandidate
                            || window.webkitRTCIceCandidate
                            || window.mozeRTCIceCandidate;

    return !!window.RTCPeerConnection;
}

// 开始连接
function startConnection() {
    if(hasUserMedia()){
        navigator.getUserMedia({
            video: true,
            audio: false
        },
        myStream => {
            stream = myStream;
            
            try{
                yourVideo.src = window.URL.createObjectURL(stream);
            }catch(e){
                yourVideo.srcObject = stream;
            }

            if(hasRTCPeerConnection()){
                setupPeerConnection(stream);
            }else{
                alert("不支持webRTC");
            }
        },
        error => {
            console.log(error);
        }
        );
    }else{
        alert("不支持webRTC");
    }
}

// 
function setupPeerConnection(stream) {
    let configuration = {
        "iceServers":[
            {"url":"stun:localhost:8888"}
        ]
    };

    yourConnection = new RTCPeerConnection(configuration);

    // 设置流量监听
    yourConnection.addStream(stream);
    yourConnection.onaddstream = function(e) {
        try{
            theirVideo.src = window.URL.createObjectURL(e.stream);
        }catch(e){
            theirVideo.srcObject = stream;
        }
    };
    // 设置ICE处理事件
    yourConnection.onicecandidate = function(event) {
        if(event.candidate){
            send({
                type: "candidate",
                candidate: event.candidate
            });
        }
    };
}

// 开始创建offer
function startPeerConnection(user) {
    connectedUser = user;

    // offer
    yourConnection.createOffer(offer=>{
        send({
            type: "offer",
            offer: offer
        });
        yourConnection.setLocalDescription(offer);
    },
    err => {
        alert("An error has occurred");
    });
}

6. WebRTC发送数据

  • 除了视频和音频,还有任意数据的传输

涵盖内容给

  • 如何理解数据通道适应webRTC难题
  • 如何在对等连接中创建一个数据通道对象
  • 加密和安全问题
  • 数据通道的潜在用例

流控制传输协议和数据传输

  • 对等连接中传输数据,使用严格的TCPAJAXWebSocket对高性能是种考验
  • 流控制传输协议(SCTP),位于UDP

堆栈图:

在这里插入图片描述

SCTP特点:

  • 传输层的安全性,基于DTLS
  • 传输层可以运行在可靠的或不可靠的模式中
  • 传输层可以担保或者无担保数据包顺序
  • 数据是面向消息进行传播的,允许消息分解传输,在接收端重组
  • 传输层支持流量和阻塞协议
  • 解决了TCP问题,利用了UDP的传输能力

规范:

  • 使用了多个端点,把消息分解成多个块进行发送数据
    • 端点:在两个IP位置之间定义任意数据的连接
    • 消息:任意从应用发送到SCTP层的数据
    • 块:正准备通过电缆传输的数据包,表示消息的一部分

在这里插入图片描述

RTCDataChannel对象

let peerConnection = new RTCPeerConnection();

//建立对等连接使用信号
let dataChannel = peerConnection.createDataChannel("myLabel",
    dataChannelOptions);

数据通道存在的状态:

  • 连接中:数据通道等待一个连接
  • 开启:连接已经被建立,可以进行通信
  • 关闭中:通道正在被销毁
  • 关闭:通道关闭,无法进行通信

ondatachannel事件

dataChannel.onerror = function (error) {
    console.log("Data channel error:", error);
};

dataChannel.onmessage = function (event) {
    console.log("Data channel message:", event.data);
}

dataChannel.onopen = function () {
    console.log("Data channel opened, ready to send message!");
    dataChannel.send("Hello World!");
}

dataChannel.onclose = function () {
    console.log("Data channel has been closed");
}
  • 数据通道和WebSocket相似

数据通道选项 - dataChannelOptions对象

let dataChannelOptions = {
    reliable: false,
    maxRetransmitTime: 3000
};
  • 这些配置使应用在UDPTCP的优势之间进行变化
    • reliable, ordered的设置为trueTCPfalseUDP
  • reliable:设置消息传递是否进行担保
  • ordered:设置消息的接受是否需要按照发送时的顺序
  • maxRetransmitTime:设置消息发送失败时,多久重新发送
  • maxRetransmit:设置消息发送失败时,最多重发次数
  • protocol:设置强制使用其他子协议
  • negotiated:设置开发人员是否有责任在两边创建数据通道,还是浏览器自动完成这个步骤
  • id:设置通道的唯一标识,在多通道时进行区分

发送数据

  • 数据通道的sendwebsocketsend
  • 支持的数据类型:
    • String
    • Blob
    • ArrayBuffer
    • ArrayBufferView
dataChannel.onmessage = function (event) {
    console.log("Data channel message:", event.data);

    let data = event.data;

    if (data instanceof Blob){

    }else if (data instanceof ArrayBuffer) {

    }else if (data instanceof ArrayBufferView){

    }else {
    //    string
    }
}

加密安全

  • WebRTC运行时,对于所有协议的实现,都会强制执行加密功能
  • 浏览器的每一个对等连接,都自动处于高的安全级别中
  • 使用DTLS

添加文字聊天

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<div id="call-page" class="page">
    <video id="yours" autoplay></video>
    <video id="theirs" autoplay></video>
    <input type="text" id="their-username">
    <button id="call">Call</button>
    <button id="hang-up">Hang Up</button>

    <input type="text" id="message">
    <button id="send">Send</button>
    <div id="received"></div>
</div>
</body>
<script src="main.js"></script>
</html>

main.js

let yourConnection = new RTCPeerConnection(),
    received = document.getElementById("received");


function openDataChannel() {
    let dataChannelOptions = {
        reliable: true
    };

    dataChannel = yourConnection.createDataChannel("myLabel", dataChannelOptions);

    dataChannel.onerror = function (error){
        console.log("Data Channel Error: ", error);
    }

    dataChannel.onmessage = function (event) {
        console.log("Got Data Channel Message: ", event.data);

        received.innerHTML += "recv: " + event.data + "<br />";
        received.scrollTop = received.scrollHeight;
    };

    dataChannel.onopen = function () {
        dataChannel.send(name + "has connected.");
    };

    dataChannel.onclose = function (){
        console.log("The data channel is closed");
    };
}
  • 添加:{optional: [{RtpDataChannels: true}]}

添加事件侦听器:

//绑定文本输入框和消息接收区
let sendButton = document.getElementById('send'),
    messageInput = document.getElementById('message');

sendButton.addEventListener('click', event => {
    let val = messageInput.value;
    received.innerHTML += "send: " + val + "<br />";
    received.scrollTop = received.scrollHeight;
    dataChannel.send(val);
});

消息框样式

#received {
    display: block;
    width: 480px;
    height: 100px;
    background: white;
    padding: 10px;
    margin-top: 10px;
    color: black;
    overflow: scroll;
}

应用

  • DTLS支持用户之间传输任何类型的数据
  • 游戏点对点通信
  • 游戏更新借助点对点通信
    • 基于流的文件共享网络,减少了昂贵的中心服务器
    • 趋于为用户之间进行大数据传输

点对点游戏的网络布局:

在这里插入图片描述

  • 用户间使用webRTC连接来传输这些文件,替代昂贵的大型网络内容分发系统(CDN)

7. 文件共享

  • 拥有点对点的数据传输能力并且与文件API相结合
  • 利用WebRTCData Channel以及文件API来构造一个简易的文件共享应用
    • 主要在两个用户(peer)间共享数据的应用
    • 该应用的基本要求是实时性,两个用户必须同时在页面上,以共享一个文件

步骤:

  1. 用户A打开页面,输入一个唯一的ID
  2. 用户B打开同样的页面,输入与A相同的ID
  3. 两个用户使用RTCPeerConnection实现互联
  4. 一旦链接建立,其中一个用户能选择一个本地文件用于共享
  5. 另一个用户会在文件共享时收到通知,共享的文件可以通过链接传输到对方的计算机并且能下载
  • 从浏览器拾取文件,将文件分块并仅使用RTCPeerConnection API来传送给另一个用户

使用文件API拾取文件

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <style>
        body{
            background-color: #404040;
            margin-top: 15px;
            font-family: sans-serif;
            color: white;
        }

        .thumb {
            height: 75px;
            border: 1px solid #000;
            margin: 10px 5px 0 0;
        }

        .page{
            position: relative;
            display: block;
            margin: 0 auto;
            width: 500px;
            height: 500px;
        }

        #byte_content {
            margin: 5px 0;
            max-height: 100px;
            overflow-y: auto;
            overflow-x: hidden;
        }

        #byte_range {
            margin-top: 5px;
        }
    </style>
</head>
<body>
<div id="login-page" class="page">
    <h2>Login As</h2>
    <input type="text" id="username">
    <button id="login">Login</button>
</div>

<div id="share-page" class="page">
    <h2>File Sharing</h2>
    <input type="text" id="their-username">
    <button id="connect">Connect</button>
    <div id="ready">Ready!</div>

    <br>
    <br>

    <input type="file" id="files" name="file"> Read bytes:
    <button id="send">Send</button>
</div>
</body>
<script src="client.js"></script>
</html>

点对点连接和数据管道

let name,
    connectedUser,
    connection = new WebSocket('ws://localhost:8888');

connection.onopen = function () {
    console.log("Connected");
};

//处理所有消息
connection.onmessage = function (message) {
    console.log("Got message:", message.data);

    let data = JSON.parse(message.data);

    switch (data.type){
        case "login":
            onLogin(data.success);
            break;
        case "offer":
            onOffer(data.offer, data.name);
            break;
        case "answer":
            onAnswer(data.answer);
            break;
        case "candidate":
            onCandidate(data.candidate);
            break;
        case "leave":
            onLeave();
            break;
        default:
            break;
    }
};

//处理错误
connection.onerror = function (err) {
    console.log("Got error:", err);
}

//JSON形式发送消息
function send(message){
    if (connectedUser){
        message.name = connectedUser;
    }

    connection.send(JSON.stringify(message));
}

let loginPage = document.querySelector('#login-page'),
    usernameInput = document.querySelector('#username'),
    loginButton = document.querySelector('#login'),
    theirUsernameInput = document.querySelector('#their-username'),
    connectButton = document.querySelector('#connect'),
    sharePage = document.querySelector('#share-page'),
    sendButton = document.querySelector('#send'),
    readyText = document.querySelector('#ready'),
    statusText = document.querySelector('#status');

sharePage.style.display = 'none';
readyText.style.display = 'none';

//用户点击按钮登录
loginButton.addEventListener('click', event=>{
    name = usernameInput.value;

    if (name.length > 0){
        send({
            type: "login",
            name: name
        });
    }
});

function onLogin(success) {
    if (success === false){
        alert("Login 失败,请使用不同的名字");
    }else{
        loginPage.style.display = 'none';
        sharePage.style.display = 'block';

    //    为每个请求建立连接
        startConnection();
    }
}

let yourConnection,
    dataChannel,
    currentFile,
    currentFileSize,
    currentFileMeta;

function startConnection() {
    if (hasRTCPeerConnection()){
        setupPeerConnection();
    }else {
        alert("不支持webRTC");
    }
}

function setupPeerConnection() {
    let configuration = {
        "iceServers":[{
            "url": "stun:localhost:8888"
        }]
    };

    yourConnection = new RTCPeerConnection(configuration, {optional:[]});

//    set up ice handling
    yourConnection.onicecandidate = function (event){
        if (event.candidate){
            send({
                type: "candidate",
                candidate: event.candidate
            });
        }
    }
    openDataChannel();
}

function openDataChannel() {
    let dataChannelOptions = {
        ordered: true,
        reliable: true,
        negotiated: true,
        id: "myChannel"
    };

    dataChannel = yourConnection.createDataChannel("myLabel", dataChannelOptions);

    dataChannel.onerror = function (error){
        console.log("Data channel error:", error);
    };

    dataChannel.onmessage = function (event) {

    }

    dataChannel.onopen = function (){
        readyText.style.display = 'inline-block';
    };

    dataChannel.onclose = function (){
        readyText.style.display = "none";
    }
}

// 函数的polyfill
function hasUserMedia() {
    navigator.getUserMedia = navigator.getUserMedia
        || navigator.webkitGetUserMedia
        || navigator.mozGetUserMedia
        || navigator.msGetUserMedia;
    return !!navigator.getUserMedia;
}

function hasRTCPeerConnection() {
    window.RTCPeerConnection = window.RTCPeerConnection
        || window.webkitRTCPeerConnection
        || window.mozRTCPeerConnection;

    window.RTCSessionDescription = window.RTCSessionDescription
        || window.webkitRTCSessionDescription
        || window.mozRTCSessionDescription;

    window.RTCIceCandidate = window.RTCIceCandidate
        || window.webkitRTCIceCandidate
        || window.mozeRTCIceCandidate;

    return !!window.RTCPeerConnection;
}

function hasFileApi() {
    return window.File && window.FileReader && window.FileList && window.Blob;
}

//事件驱动
connectButton.addEventListener("click", ()=>{
    let theirUsername = theirUsernameInput.value;

    if (theirUsername.length > 0){
        startPeerConnection(theirUsername);
    }
});

function startPeerConnection(user) {
    connectedUser = user;

//    开始新建连接邀请
    yourConnection.createOffer(offer=>{
        send({
            type: "offer",
            offer: offer
        });

        yourConnection.setLocalDescription(offer);
    },
    error=>{
        alert("error");
    });
}

function onOffer(offer, name) {
    connectedUser = name;
    yourConnection.setRemoteDescription(new RTCSessionDescription(offer));

    yourConnection.createAnswer(answer=>{
        yourConnection.setLocalDescription(answer);

        send({
            type: "answer",
            answer: answer
        });
    },
    error =>    alert("error")
    );
}

function onAnswer(answer) {
    yourConnection.setRemoteDescription(new RTCSessionDescription(answer));
}

function onCandidate(candidate) {
    yourConnection.addIceCandidate(new RTCIceCandidate(candidate));
}

function onLeave() {
    connectedUser = null;

    yourConnection.close();
    yourConnection.onicecandidate = null;
    setupPeerConnection();
}
  • 通过发送邀请和响应,将两个用户连接在一起
  • 连接建立,可以在数据通道来回发送任意二进制数据信息

获取对文件的引用

let name,
    connectedUser,
    connection = new WebSocket('ws://localhost:8888');

connection.onopen = function () {
    console.log("Connected");
};

//处理所有消息
connection.onmessage = function (message) {
    console.log("Got message:", message.data);

    let data = JSON.parse(message.data);

    switch (data.type){
        case "login":
            onLogin(data.success);
            break;
        case "offer":
            onOffer(data.offer, data.name);
            break;
        case "answer":
            onAnswer(data.answer);
            break;
        case "candidate":
            onCandidate(data.candidate);
            break;
        case "leave":
            onLeave();
            break;
        default:
            break;
    }
};

//处理错误
connection.onerror = function (err) {
    console.log("Got error:", err);
}

//JSON形式发送消息
function send(message){
    if (connectedUser){
        message.name = connectedUser;
    }

    connection.send(JSON.stringify(message));
}

let loginPage = document.querySelector('#login-page'),
    usernameInput = document.querySelector('#username'),
    loginButton = document.querySelector('#login'),
    theirUsernameInput = document.querySelector('#their-username'),
    connectButton = document.querySelector('#connect'),
    sharePage = document.querySelector('#share-page'),
    sendButton = document.querySelector('#send'),
    readyText = document.querySelector('#ready'),
    statusText = document.querySelector('#status');

sharePage.style.display = 'none';
readyText.style.display = 'none';

//用户点击按钮登录
loginButton.addEventListener('click', event=>{
    name = usernameInput.value;

    if (name.length > 0){
        send({
            type: "login",
            name: name
        });
    }
});

function onLogin(success) {
    if (success === false){
        alert("Login 失败,请使用不同的名字");
    }else{
        loginPage.style.display = 'none';
        sharePage.style.display = 'block';

    //    为每个请求建立连接
        startConnection();
    }
}

let yourConnection,
    dataChannel,
    currentFile,
    currentFileSize,
    currentFileMeta;

function startConnection() {
    if (hasRTCPeerConnection()){
        setupPeerConnection();
    }else {
        alert("不支持webRTC");
    }
}

function setupPeerConnection() {
    let configuration = {
        "iceServers":[{
            "url": "stun:localhost:8888"
        }]
    };

    yourConnection = new RTCPeerConnection(configuration, {optional:[]});

//    set up ice handling
    yourConnection.onicecandidate = function (event){
        if (event.candidate){
            send({
                type: "candidate",
                candidate: event.candidate
            });
        }
    }
    openDataChannel();
}

function openDataChannel() {
    let dataChannelOptions = {
        ordered: true,
        reliable: true,
        negotiated: true,
        id: "myChannel"
    };

    dataChannel = yourConnection.createDataChannel("myLabel", dataChannelOptions);

    dataChannel.onerror = function (error){
        console.log("Data channel error:", error);
    };

    dataChannel.onmessage = function (event) {

    }

    dataChannel.onopen = function (){
        readyText.style.display = 'inline-block';
    };

    dataChannel.onclose = function (){
        readyText.style.display = "none";
    }
}

// 函数的polyfill
function hasUserMedia() {
    navigator.getUserMedia = navigator.getUserMedia
        || navigator.webkitGetUserMedia
        || navigator.mozGetUserMedia
        || navigator.msGetUserMedia;
    return !!navigator.getUserMedia;
}

function hasRTCPeerConnection() {
    window.RTCPeerConnection = window.RTCPeerConnection
        || window.webkitRTCPeerConnection
        || window.mozRTCPeerConnection;

    window.RTCSessionDescription = window.RTCSessionDescription
        || window.webkitRTCSessionDescription
        || window.mozRTCSessionDescription;

    window.RTCIceCandidate = window.RTCIceCandidate
        || window.webkitRTCIceCandidate
        || window.mozeRTCIceCandidate;

    return !!window.RTCPeerConnection;
}

function hasFileApi() {
    return window.File && window.FileReader && window.FileList && window.Blob;
}

//事件驱动
connectButton.addEventListener("click", ()=>{
    let theirUsername = theirUsernameInput.value;

    if (theirUsername.length > 0){
        startPeerConnection(theirUsername);
    }
});

function startPeerConnection(user) {
    connectedUser = user;

//    开始新建连接邀请
    yourConnection.createOffer(offer=>{
        send({
            type: "offer",
            offer: offer
        });

        yourConnection.setLocalDescription(offer);
    },
    error=>{
        alert("error");
    });
}

function onOffer(offer, name) {
    connectedUser = name;
    yourConnection.setRemoteDescription(new RTCSessionDescription(offer));

    yourConnection.createAnswer(answer=>{
        yourConnection.setLocalDescription(answer);

        send({
            type: "answer",
            answer: answer
        });
    },
    error =>    alert("error")
    );
}

function onAnswer(answer) {
    yourConnection.setRemoteDescription(new RTCSessionDescription(answer));
}

function onCandidate(candidate) {
    yourConnection.addIceCandidate(new RTCIceCandidate(candidate));
}

function onLeave() {
    connectedUser = null;

    yourConnection.close();
    yourConnection.onicecandidate = null;
    setupPeerConnection();
}

sendButton.addEventListener("click", event=>{
    let files = document.querySelector('#files').files;

    if(files.length > 0){
        dataChannelSend({
            type: "start",
            data: files[0]
        });

        sendFile(files[0]);
    }
});
  • dataChannelSend, sendFile暂时不写
  • 此时可以获取文件的信息

文件分块

  • 数据通道通过SCTP协议进行传输文件
  • 如果网络不佳,会导致文件丢失
  • 利用BitTorrent实现文件块切分,每次只传小块
  • 必须在使用数据通道前使用这一步

文件分块可读

  • 因为JS底层要求使用字符串格式,需要采用Base64编码
  • 发送,转换为Base64编码 -> 传送过去另一个用户,解码得到结果

编码:

function arrayBufferToBase64(buffer) {
    var binary = '';
    var bytes = new Uint8Array( buffer );
    var len = bytes.byteLength;
    for (var i = 0; i < len; i++) {
        binary += String.fromCharCode( bytes[ i ] );
    }
    return btoa(binary);
}
  • 参数为ArrayBuffer对象,文件API读取文件内容时的返回值
  • fromCharcode转为字符
  • bota编码

解码:

function base64ToBlob(b64Data, contentType) {
    contentType = contentType || '';

    var byteArrays = [], byteNumbers, slice;

    for (var i = 0; i < b64Data.length; i++) {
        slice = b64Data[i];

        byteNumbers = new Array(slice.length);
        for (var n = 0; n < slice.length; n++) {
            byteNumbers[n] = slice.charCodeAt(n);
        }

        var byteArray = new Uint8Array(byteNumbers);

        byteArrays.push(byteArray);
    }

    var blob = new Blob(byteArrays, {type: contentType});
    return blob;
}
  • charCodeAt每一个字符转化成二进制数据
  • 得到翻译后的数组后,将其转换为Blob,因此JS可以对数据进行交互,甚至保存为文件

文件读取和发送

  • 从文件中读取二进制数据,并且发送给另一个用户
  • 数据通道和Base64编码有效结合
var CHUNK_MAX = 16000;
function sendFile(file) {
    var reader = new FileReader();

    reader.onloadend = function(evt) {
        if (evt.target.readyState == FileReader.DONE) {
            var buffer = reader.result,
                start = 0,
                end = 0,
                last = false;

            function sendChunk() {
                end = start + CHUNK_MAX;

                if (end > file.size) {
                    end = file.size;
                    last = true;
                }

                var percentage = Math.floor((end / file.size) * 100);
                statusText.innerHTML = "Sending... " + percentage + "%";

                dataChannel.send(arrayBufferToBase64(buffer.slice(start, end)));

                // If this is the last chunk send our end message, otherwise keep sending
                if (last === true) {
                    dataChannelSend({
                        type: "end"
                    });
                } else {
                    start = end;
                    // Throttle the sending to avoid flooding
                    setTimeout(function () {
                        sendChunk();
                    }, 100);
                }
            }

            sendChunk();
        }
    };

    reader.readAsArrayBuffer(file);
}
  • 实例化FileReader对象
  • 封装了JavaScript中使用不同格式读取文件的方法
  • ArrayBuffer的形式读取二进制文件

文件读取的流程:

  1. 确认FileReader对象在DONE状态
  2. 初始化并获取文件数据的缓冲区引用
  3. 建立一个递归函数,实现发送文件块的功能
  4. 在函数中,从0开始读取一个文件块的字节
  5. 确保没有超过文件尾,否则没有数据可读
  6. 将数据通过Base64格式进行编码,并且进行发送
  7. 如果是最后一个文件块,告诉另一个用户已经完成文件发送
  8. 还有数据需要传输,在固定的时间后发送另一个分块防止API发生洪泛
  9. sendChunk开始递归

在接收端组合文件块

  • 由于在数据通道中使用了Ordered选项,用户收到的文件分块是有序的
  • 解码函数将文件组合
dataChannel.onmessage = function (event) {
    try {
        var message = JSON.parse(event.data);

        switch (message.type) {
            case "start":
                currentFile = [];
                currentFileSize = 0;
                currentFileMeta = message.data;
                console.log("Receiving file", currentFileMeta);
                break;
            case "end":
                saveFile(currentFileMeta, currentFile);
                break;
        }
        //如果是中间文件,将进入报错处理环节
    } catch (e) {
        // Assume this is file content
        currentFile.push(atob(event.data));

        currentFileSize += currentFile[currentFile.length - 1].length;

        var percentage = Math.floor((currentFileSize / currentFileMeta.size) * 100);
        statusText.innerHTML = "Receiving... " + percentage + "%";
    }
};

文件自动下载

function saveFile(meta, data) {
    var blob = base64ToBlob(data, meta.type);
    console.log(blob);

    var link = document.createElement('a');
    link.href = window.URL.createObjectURL(blob);
    link.download = meta.name;
    link.click();
}
  • 文件数据转化成几个块
  • 新建link对象,url指向数据,模拟点击下载
  • createObjectURL创建伪位置

向用户展示进度

if (end > file.size) {
    end = file.size;
    last = true;
}

var percentage = Math.floor((end / file.size) * 100);
statusText.innerHTML = "Sending... " + percentage + "%";

接受端:

// Assume this is file content
currentFile.push(atob(event.data));

currentFileSize += currentFile[currentFile.length - 1].length;

var percentage = Math.floor((currentFileSize / currentFileMeta.size) * 100);
statusText.innerHTML = "Receiving... " + percentage + "%";

总结

var name,
    connectedUser;

var connection = new WebSocket('ws://localhost:8888');

connection.onopen = function () {
    console.log("Connected");
};

// Handle all messages through this callback
connection.onmessage = function (message) {
    console.log("Got message", message.data);

    var data = JSON.parse(message.data);

    switch(data.type) {
        case "login":
            onLogin(data.success);
            break;
        case "offer":
            onOffer(data.offer, data.name);
            break;
        case "answer":
            onAnswer(data.answer);
            break;
        case "candidate":
            onCandidate(data.candidate);
            break;
        case "leave":
            onLeave();
            break;
        default:
            break;
    }
};

connection.onerror = function (err) {
    console.log("Got error", err);
};

// Alias for sending messages in JSON format
function send(message) {
    if (connectedUser) {
        message.name = connectedUser;
    }

    connection.send(JSON.stringify(message));
};

var loginPage = document.querySelector('#login-page'),
    usernameInput = document.querySelector('#username'),
    loginButton = document.querySelector('#login'),
    theirUsernameInput = document.querySelector('#their-username'),
    connectButton = document.querySelector('#connect'),
    sharePage = document.querySelector('#share-page'),
    sendButton = document.querySelector('#send'),
    readyText = document.querySelector('#ready'),
    statusText = document.querySelector('#status');

sharePage.style.display = "none";
readyText.style.display = "none";

// Login when the user clicks the button
loginButton.addEventListener("click", function (event) {
    name = usernameInput.value;

    if (name.length > 0) {
        send({
            type: "login",
            name: name
        });
    }
});

function onLogin(success) {
    if (success === false) {
        alert("Login unsuccessful, please try a different name.");
    } else {
        loginPage.style.display = "none";
        sharePage.style.display = "block";

        // Get the plumbing ready for a call
        startConnection();
    }
};

var yourConnection, connectedUser, dataChannel, currentFile, currentFileSize, currentFileMeta;

function startConnection() {
    if (hasRTCPeerConnection()) {
        setupPeerConnection();
    } else {
        alert("Sorry, your browser does not support WebRTC.");
    }
}

function setupPeerConnection() {
    var configuration = {
        "iceServers": [{ "url": "stun:127.0.0.1:8888" }]
    };
    yourConnection = new RTCPeerConnection(configuration, {optional: []});

    // Setup ice handling
    yourConnection.onicecandidate = function (event) {
        if (event.candidate) {
            send({
                type: "candidate",
                candidate: event.candidate
            });
        }
    };

    openDataChannel();
}

function openDataChannel() {
    var dataChannelOptions = {
        ordered: true,
        reliable: true,
        negotiated: true,
        id: 0
    };
    dataChannel = yourConnection.createDataChannel('myLabel', dataChannelOptions);

    dataChannel.onerror = function (error) {
        console.log("Data Channel Error:", error);
    };

    dataChannel.onmessage = function (event) {
        try {
            var message = JSON.parse(event.data);

            switch (message.type) {
                case "start":
                    currentFile = [];
                    currentFileSize = 0;
                    currentFileMeta = message.data;
                    console.log(`message.data: `)
                    console.log(message.data)
                    console.log("Receiving file", currentFileMeta);
                    break;
                case "end":
                    saveFile(currentFileMeta, currentFile);
                    break;
            }
        } catch (e) {
            // Assume this is file content
            currentFile.push(atob(event.data));

            currentFileSize += currentFile[currentFile.length - 1].length;

            var percentage = Math.floor((currentFileSize / currentFileMeta.size) * 100);
            statusText.innerHTML = "Receiving... " + percentage + "%";
        }
    };

    dataChannel.onopen = function () {
        readyText.style.display = "inline-block";
    };

    dataChannel.onclose = function () {
        readyText.style.display = "none";
    };
}

// Alias for sending messages in JSON format
function dataChannelSend(message) {
    dataChannel.send(JSON.stringify(message));
}

function saveFile(meta, data) {
    var blob = base64ToBlob(data, meta.type);
    console.log(blob);

    var link = document.createElement('a');
    link.href = window.URL.createObjectURL(blob);
    link.download = meta.name;
    link.click();
}

function hasUserMedia() {
    navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia;
    return !!navigator.getUserMedia;
}

function hasRTCPeerConnection() {
    window.RTCPeerConnection = window.RTCPeerConnection || window.webkitRTCPeerConnection || window.mozRTCPeerConnection;
    window.RTCSessionDescription = window.RTCSessionDescription || window.webkitRTCSessionDescription || window.mozRTCSessionDescription;
    window.RTCIceCandidate = window.RTCIceCandidate || window.webkitRTCIceCandidate || window.mozRTCIceCandidate;
    return !!window.RTCPeerConnection;
}

function hasFileApi() {
    return window.File && window.FileReader && window.FileList && window.Blob;
}

connectButton.addEventListener("click", function () {
    var theirUsername = theirUsernameInput.value;

    if (theirUsername.length > 0) {
        startPeerConnection(theirUsername);
    }
});

function startPeerConnection(user) {
    connectedUser = user;

    // Begin the offer
    yourConnection.createOffer(function (offer) {
        send({
            type: "offer",
            offer: offer
        });
        yourConnection.setLocalDescription(offer);
    }, function (error) {
        alert("An error has occurred.");
    });
};

function onOffer(offer, name) {
    connectedUser = name;
    yourConnection.setRemoteDescription(new RTCSessionDescription(offer));

    yourConnection.createAnswer(function (answer) {
        yourConnection.setLocalDescription(answer);

        send({
            type: "answer",
            answer: answer
        });
    }, function (error) {
        alert("An error has occurred");
    });
};

function onAnswer(answer) {
    yourConnection.setRemoteDescription(new RTCSessionDescription(answer));
};

function onCandidate(candidate) {
    yourConnection.addIceCandidate(new RTCIceCandidate(candidate));
};

function onLeave() {
    connectedUser = null;
    yourConnection.close();
    yourConnection.onicecandidate = null;
    setupPeerConnection();
};

function arrayBufferToBase64(buffer) {
    var binary = '';
    var bytes = new Uint8Array( buffer );
    var len = bytes.byteLength;
    for (var i = 0; i < len; i++) {
        binary += String.fromCharCode( bytes[ i ] );
    }
    return btoa(binary);
}

function base64ToBlob(b64Data, contentType) {
    contentType = contentType || '';

    var byteArrays = [], byteNumbers, slice;

    for (var i = 0; i < b64Data.length; i++) {
        slice = b64Data[i];

        byteNumbers = new Array(slice.length);
        for (var n = 0; n < slice.length; n++) {
            byteNumbers[n] = slice.charCodeAt(n);
        }

        var byteArray = new Uint8Array(byteNumbers);

        byteArrays.push(byteArray);
    }

    var blob = new Blob(byteArrays, {type: contentType});
    return blob;
}

var CHUNK_MAX = 16000;
function sendFile(file) {
    var reader = new FileReader();

    reader.onloadend = function(evt) {
        if (evt.target.readyState == FileReader.DONE) {
            var buffer = reader.result,
                start = 0,
                end = 0,
                last = false;

            function sendChunk() {
                end = start + CHUNK_MAX;

                if (end > file.size) {
                    end = file.size;
                    last = true;
                }

                var percentage = Math.floor((end / file.size) * 100);
                statusText.innerHTML = "Sending... " + percentage + "%";

                dataChannel.send(arrayBufferToBase64(buffer.slice(start, end)));

                // If this is the last chunk send our end message, otherwise keep sending
                if (last === true) {
                    dataChannelSend({
                        type: "end"
                    });
                } else {
                    start = end;
                    // Throttle the sending to avoid flooding
                    setTimeout(function () {
                        sendChunk();
                    }, 100);
                }
            }

            sendChunk();
        }
    };

    reader.readAsArrayBuffer(file);
}

sendButton.addEventListener("click", function (event) {
    var files = document.querySelector('#files').files;

    if (files.length > 0) {
        dataChannelSend({
            type: "start",
            data: files[0],
            name: files[0].name
        });
        console.log(`datachannel start[0]: `)
        console.log(files[0])
        sendFile(files[0]);
    }
});

8. 高安全性和大规模优化

  • 深入研究和阅读
  • 以服务器为例

保护信令服务器

  • 还需要根据特性进行修改

使用编码

  • 强制加密,对信令服务器的消息进行加密
  • 加密方式HTTPS, WSS
  • HTTPSHTTP进行的SSL加密方式
  • WSSTLS上基于WebSocketSSL加密方式

OAuth提供器

  • 集成一个第三方身份提供器
  • 背后原理:token是一个包含数字和字母的随机字符串

实现前的考虑:

  • 双方用户在形成链接之前都必须登录
  • 双方用户必须相互认识才能被允许进行对话
  • 双方用户在对话时不会受到网络攻击的愚弄

移动设备

  • 减少数据量,减少视频大小
let mobile = {
    video: {
        mandatory: {
            maxWidth: 640,
            maxHeight: 360
        }
    }
};

let desktop = {
    video: {
        mandatory: {
            minWidth: 1280,
            minHeight: 720
        }
    }
};

let constraints;

if (/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|OperaMini/i.test(navigator.userAgent)) {
    constraints = mobile;
}else {
    constraints = desktop;
}

navigator.getUserMedia(constraints, success, function (error)){

};

网格网络

  • 一对一变成一对多
  • 网格:每一个节点和其他所有节点在一个全网格进行对话

测试带宽

var src = "example-image.jpg",
    size = 5000000,
    image = new Image(),
    startTime,
    endTime,
    totalTime = 0,
    speed = 0;

image.onload = function () {
  endTime = (new Date()).getTime();
  totalTime = (endTime - startTime) / 1000;
  speed = (size * 8 / totalTime); // bytes per second
};

startTime = (new Date()).getTime();
image.src = src + "?cacheBust=" + startTime;


  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Develop interactive real-time communication applications with WebRTC About This Book Create easy-to-use real-time communication applications using WebRTC platform Learn the best practices behind traversing networks, signaling, security, and data transport in WebRTC applications Use full mesh networks, partial mesh networks, and multi point control units to manage your applications Who This Book Is For If you are a web developer who wants to create well designed WebRTC applications for your users, this is the book for you. Even if you are early in your web development career, this book aims to provide a complete understanding of the WebRTC API. It is assumed that you have previous exposure to web development using HTML5 and JavaScript technologies. In Detail The book begins by teaching you how to capture audio and video streams from the browser using the Media Capture and Streams API. You will then create your first WebRTC application capable of audio and video calling. The book will also give you in-depth knowledge about signaling and building a signaling server in Node.js. While being introduced to the RTCDataChannel object, you will learn how it relates to WebRTC and how to add text-based chat to your application. You will also learn to take your application further by supporting multiple users through different technologies and scale its performance and security. This book will also cover several theories using full mesh networks, partial mesh networks, and multipoint control units. By the end of this book, you will have an extensive understanding of real-time communication and the WebRTC protocol and APIs. Table of Contents Chapter 1. Getting Started with WebRTC Chapter 2. Getting the User's Media Chapter 3. Creating a Basic WebRTC Application Chapter 4. Creating a Signaling Server Chapter 5. Connecting Clients Together Chapter 6. Sending Data with WebRTC Chapter 7. File Sharing Chapter 8. Advanced Security and Large-scale Optimization

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值