webRTC- LearningWebRTC-读书笔记

LearningWebRTC 目录

文章目录

  • 前言
    • 前提
    • 推荐阅读
  • 总结
  • 1. 开启WebRTC之旅
    • 在WebRTC平台传输音频和视频
    • WebRTC的应用
  • 2. 获取用户媒体
    • 配置静态服务器
    • 媒体流页面
      • 注意
    • 媒体流的方法- getUserMedia API
      • API处理方法
      • 限制视频捕捉
        • 推荐阅读
        • 应用实例
      • 多设备处理
    • 创建一个拍照
    • 修改媒体流
  • 3. 创建简单的WebRTC应用
    • 本章内容
    • 理解`UDP`传输协议和实时传输
    • WebRTC API
    • RTCPeerConnection对象
    • 信号传递和交涉
    • 会话描述协议-SDP
    • 清晰的路线到用户
    • `STUN`
    • `TURN`
    • `ICE`
    • WebRTC应用
      • `RTCPeerConnection`
        • `index.html`
        • `main.js`
        • 执行状态流程图
        • 捕获用户的摄像头
        • `RTCPeerConnection`对象
        • 建立`SDP Offer`和返回
        • 寻找`ICE`候选路径
        • 加入流和打磨
        • 增加`css`样式
      • 代码总结
  • 4. 创建信令服务器
    • 涵盖内容
    • 构建信令服务器
    • `WebSockets`
      • `server.js`
    • 识别用户
    • 存储对象
    • `login`登录
    • 发起通话
    • 呼叫应答
    • 处理`ICE`候选路径
    • 呼叫挂断
    • 完整的信令服务器
    • `websocket`困境
    • 代替技术 - `XMPP, SIP`
  • 5. 连接客户端
    • 涵盖内容
    • 连接
    • 创建页面
      • `index.html`
    • 获取一个连接
    • 登录
    • 开始对等连接
    • 发起通话
    • 检测通信
    • 挂断电话
    • 总结
  • 6. `WebRTC`发送数据
    • 涵盖内容给
    • 流控制传输协议和数据传输
    • `RTCDataChannel`对象
      • `ondatachannel`事件
    • 数据通道选项 - `dataChannelOptions`对象
    • 发送数据
    • 加密安全
    • 添加文字聊天
      • `index.html`
      • `main.js`
      • 消息框样式
    • 应用
  • 7. 文件共享
    • 使用文件`API`拾取文件
      • `index.html`
    • 点对点连接和数据管道
    • 获取对文件的引用
    • 文件分块
    • 文件分块可读
    • 文件读取和发送
    • 在接收端组合文件块
    • 文件自动下载
    • 向用户展示进度
    • 总结
  • 8. 高安全性和大规模优化
    • 保护信令服务器
      • 使用编码
      • `OAuth`提供器
    • 移动设备
    • 网格网络
    • 测试带宽


前言

  • 适合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>

推荐阅读

  • 《Learning WebRTC》
  • WebRTC作者Github
  • 博客
  • WebRTC
  • 编解码器
  • W3C-webrtc具体细节

总结

  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:


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

注意

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

媒体流的方法- getUserMedia API

API处理方法

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

限制视频捕捉

推荐阅读

  • getUserMedia API
    例如:
  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可以绘制线条、图形和图片,可以制作游戏

<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 Mediatitle>
    <style>

        video, canvas{
            border: 1px solid gray;
            width: 480px;
            height: 320px;
        }
    style>
head>
<body>
    <video autoplay>video>    
    <canvas>canvas>
    <button id="capture">Capturebutton>
    <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连接
  • 功能:
    • 维护浏览器内会话和对等连接的状态
    • 对等连接建立

webRTC- LearningWebRTC-读书笔记_第1张图片

实例化对象:

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

信号传递和交涉

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

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

会话描述协议-SDP

  • 用户需要传出信息指明视频解码器,何种网络。
  • SDP是基于字符串的二进制数据对象:=\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连接过程的架构:
webRTC- LearningWebRTC-读书笔记_第2张图片

  • 找到ip

STUN

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

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

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

TURN

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

ICE

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

WebRTC应用

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

RTCPeerConnection

index.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 WebRTCtitle>
head>
<body>
    <div id="container">
        <video id="yours" autoplay>video>
        <video id="theirs" autoplay>video>
    div>
    <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;
}

webRTC- LearningWebRTC-读书笔记_第3张图片

代码总结

index.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 WebRTCtitle>
    <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>
    
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,或者前端测试


<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>Documenttitle>
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!"}

webRTC- LearningWebRTC-读书笔记_第4张图片

处理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


<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>Documenttitle>
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 Ash2>
        <input type="text" id="username">
        <button id="login">Loginbutton>
    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">Callbutton>
        <button id="hang-up">Hang Upbutton>
    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

堆栈图:

webRTC- LearningWebRTC-读书笔记_第5张图片

SCTP特点:

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

规范:

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

webRTC- LearningWebRTC-读书笔记_第6张图片

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


<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Titletitle>
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">Callbutton>
    <button id="hang-up">Hang Upbutton>

    <input type="text" id="message">
    <button id="send">Sendbutton>
    <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 + "
"
; 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 + "
"
; 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- LearningWebRTC-读书笔记_第7张图片

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

7. 文件共享

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

步骤:

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

使用文件API拾取文件

index.html


<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Titletitle>
    <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 Ash2>
    <input type="text" id="username">
    <button id="login">Loginbutton>
div>

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

    <br>
    <br>

    <input type="file" id="files" name="file"> Read bytes:
    <button id="send">Sendbutton>
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;


你可能感兴趣的:(前端,JS,WEB,webrtc)