LearningWebRTC 目录
HTML
和JavaScript
构建Web
应用程序的有经验者<!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>
RTCPeerConnection, ICE, SDP, offer, answer
STUN
信令服务器未来方向:
本章基础知识:
需要考虑:
- 连接断开
- 数据丢失
- NAT穿透
API:
- 捕捉摄像头和麦克风
- 音视频解码
- 传输层
- 会话管理
主要内容:
npm install -g node-static
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
作为参数,只能通过一些字符串来换取视频流
元素中应该包含一个autoplay
属性,表示自动播放stream
对象并导入页面上的视频元素这个过程,如果用C/C++是非常繁琐的navigator.getUserMedia({video: false, audio: true}, function (stream){
// 视频流里不包含视频
})
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");
}
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);
});
MediaSourceTrack
的APIMediaStreamTrack.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}`)
);
});
<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("对不起,您的浏览器不支持");
}
.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);
RTCPeerConnection
RTCPeerConnection
的前提,就是需要理解浏览器创建对等连接的内部工作原理UDP
传输协议和实时传输RTCPeerConnection
UDP
传输协议和实时传输实时传输要求双方间有快速的连接速度。
主要技术:
API
的主入口
实例化对象:
let myConnection = new RTCPeerConnection(configuration);
myConnection.onaddstream = stream => console.log(stream);
网络地址:IP
地址和端口号组成
发送信令的过程:
=\n
sample:
let configuration = {
bundlePolicy: "max-compat"
};
let myConnection = new RTCPeerConnection(configuration);
myConnection.onaddstream = function(stream) {
console.log(stream);
};
使用的多种技术:
ip
STUN
使用STUN协议需要由一个支持STUN协议的服务器
TURN
TURN
服务器去下载、处理并重定向每一个用户发送过来的数据包ICE
STUN
和TURN
结合后的标准ICE
候选路径都是通过STUN
和TURN
来找到的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;
}
offer
,另一方做准备。offer
和返回都是发送信令过程中的一部分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
<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");
});
}
WebRTC
应用,需要抛开客户端的开发,转而为服务端的开发Node.js
WebSocket
WebRTC
通话ICE
候选路径的传送WebRTC
连接ICE
候选路径流程:
注意:
WebSockets
建立WebRTC
连接所需的步骤必须是实时的,最好使用WebSockets
,不能使用WebRTC
对等连接实时传递消息
Socket
以字符串和二进制码方式双向发送信息
完全依赖于WebSocket
框架:Meteor JavaScript framework
npm
安装websocket
:npm install ws
wscat
:npm 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
处理器,用户用来呼叫另一方
呼叫初始化的过程和WebRTC
的offer
分开最好,但此处结合
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;
讲解:
connection
对象offer
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
,将会扰乱服务器的实现webRTC
的RTCPeerConnection
的createOffer
和createAnswer
测试:
# 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
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();
}
WebRTC
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));
}
调试实时应用困难在:许多事件发生在同一时刻,要完整描述某一时刻发生了什么很难
Chrome
中View
->Developer
->Developer Tools
可以看到webSocket
得通信状态
过程:
RTCPeerConnection
关闭,停止发送数据流给其他用户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");
});
}
WebRTC
发送数据webRTC
难题TCP
,AJAX
和WebSocket
对高性能是种考验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
};
UDP
或TCP
的优势之间进行变化
reliable, ordered
的设置为true
像TCP
,false
像UDP
reliable
:设置消息传递是否进行担保ordered
:设置消息的接受是否需要按照发送时的顺序maxRetransmitTime
:设置消息发送失败时,多久重新发送maxRetransmit
:设置消息发送失败时,最多重发次数protocol
:设置强制使用其他子协议negotiated
:设置开发人员是否有责任在两边创建数据通道,还是浏览器自动完成这个步骤id
:设置通道的唯一标识,在多通道时进行区分send
像websocket
的send
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
连接来传输这些文件,替代昂贵的大型网络内容分发系统(CDN)API
相结合WebRTC
的Data Channel
以及文件API
来构造一个简易的文件共享应用
步骤:
A
打开页面,输入一个唯一的ID
号B
打开同样的页面,输入与A
相同的ID
号RTCPeerConnection
实现互联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
的形式读取二进制文件文件读取的流程:
FileReader
对象在DONE
状态Base64
格式进行编码,并且进行发送API
发生洪泛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]);
}
});
HTTPS, WSS
HTTPS
是HTTP
进行的SSL
加密方式WSS
是TLS
上基于WebSocket
的SSL
加密方式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;