一、RTCDataChannel

  • WebRTC 不但可以让你进行音视频通话,而且还可以用它传输普通的二进制数据,比如说可以利用它实现文本聊天、文件的传输等

  • WebRTC 的数据通道(RTCDataChannel)是专门用来传输除了音视频数据之外的任何数据,模仿了 WebSocket 的实现

  • RTCDataChannel 支持的数据类型也非常多,包括:字符串BlobArrayBuffer 以及 ArrayBufferView

  • WebRTC RTCDataChannel 使用的传输协议为 SCTP,即 Stream Control Transport Protocol

  • RTCDataChannel 既可以在可靠的、有序的模式下工作,也可在不可靠的、无序的模式下工作

  • 可靠有序模式(TCP 模式):在这种模式下,消息可以有序到达,但同时也带来了额外的开销,所以在这种模式下消息传输会比较慢

  • 不可靠无序模式(UDP 模式):在此种模式下,不保证消息可达,也不保证消息有序,但在这种模式下没有什么额外开销,所以它非常快

  • 部分可靠模式(SCTP 模式):在这种模式下,消息的可达性和有序性可以根据业务需求进行配置

  • RTCDataChannel 对象是由 RTCPeerConnection 对象创建,其中包含两个参数:

  • 第一个参数:是一个标签(字符串),相当于给 RTCDataChannel 起了一个名字

  • 第二个参数:是 options,包含很多配置,其中就可以设置上面说的模式,重试次数等

// 创建 RTCPeerConnection 对象
var pc = new RTCPeerConnection(); // 创建 RTCDataChannel 对象
var dc = pc.createDataChannel("dc", {
ordered: true // 保证到达顺序
}); // options参数详解, 前三项是经常使用的:
// ordered:消息的传递是否有序
// maxPacketLifeTime:重传消息失败的最长时间
// maxRetransmits:重传消息失败的最大次数
// protocol:用户自定义的子协议, 默认为空
// negotiated:如果为 true,则会删除另一方数据通道的自动设置
// id:当 negotiated 为 true 时,允许你提供自己的 ID 与 channel 进行绑定 // dc的事件处理与 WebSocket 的事件处理非常相似
dc.onerror = (error) => {
// 出错的处理
};
dc.onopen = () => {
// 打开的处理
};
dc.onclose = () => {
// 关闭的处理
};
dc.onmessage = (event) => {
// 收到消息的处理
var msg = event.data;
};

二、文本聊天

  • 点击 Start 按钮时,会调用 start方法获取视频流然后 调用 conn 方法

  • 然后调用 io.connect() 连接信令服务器,然后再根据信令服务器下发的消息做不同的处理

  • 数据的发送非常简单,当用户点击 Send 按钮后,文本数据就会通过 RTCDataChannel 传输到远端

  • 对于接收数据,则是通过 RTCDataChannel onmessage 事件实现的

  • RTCDataChannel 对象的创建要在媒体协商(offer/answer) 之前创建,否则 WebRTC 就会一直处于 connecting 状态,从而导致数据无法进行传输

  • RTCDataChannel 对象是可以双向传输数据的,所以接收与发送使用一个RTCDataChannel 对象即可,而不需要为发送和接收单独创建 RTCDataChannel 对象

<!DOCTYPE html>
<html lang="en"> <head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
.preview {
display: flex;
} .remote {
margin-left: 20px;
} .text_chat {
display: flex;
} .text_chat textarea {
width: 350px;
height: 350px;
} .send {
margin-top: 20px;
}
</style>
</head> <body>
<div>
<div>
<button onclick="start()">连接信令服务器</button>
<button onclick="leave()" disabled>断开连接</button>
</div> <div class="preview">
<div>
<h2>本地:</h2>
<video id="localvideo" autoplay playsinline></video>
</div>
<div class="remote">
<h2>远端:</h2>
<video id="remotevideo" autoplay playsinline></video>
</div>
</div>
<!--文本聊天-->
<h2>聊天:</h2>
<div class="text_chat">
<div>
<textarea id="chat" disabled></textarea>
</div>
<div class="remote">
<textarea id="sendtext" disabled></textarea>
</div>
</div>
<div class="send">
<button onclick="send()" disabled>发送</button>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.0.3/socket.io.js"></script>
<script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
</body>
<script>
'use strict' var localVideo = document.querySelector('video#localvideo');
var remoteVideo = document.querySelector('video#remotevideo'); // 文本聊天
var chat = document.querySelector('textarea#chat');
var send_txt = document.querySelector('textarea#sendtext'); var localStream = null; var roomid = '44444';
var socket = null; var state = 'init'; var pc = null;
var dc = null; function sendMessage(roomid, data) {
socket.emit('message', roomid, data);
} function getAnswer(desc) {
pc.setLocalDescription(desc);
// 发送信息
socket.emit('message', roomid, desc);
} function handleAnswerError(err) {
console.error('Failed to get Answer!', err);
} //接收远端流通道
function call() {
if (state === 'joined_conn') {
if (pc) {
var options = {
offerToReceiveAudio: 1,
offerToReceiveVideo: 1
}
pc.createOffer(options)
.then(function (desc) {
pc.setLocalDescription(desc);
socket.emit('message', roomid, desc);
})
.catch(function (err) {
console.error('Failed to get Offer!', err);
});
}
}
} //文本对方传过来的数据
function reveivemsg(e) {
var msg = e.data;
console.log('recreived msg is :' + e.data);
if (msg) {
chat.value += '->' + msg + '\r\n';
} else {
console.error('recreived msg is null');
}
} function dataChannelStateChange() {
var readyState = dc.readyState;
if (readyState === 'open') {
send_txt.disabled = false;
btnSend.disabled = false;
} else {
send_txt.disabled = true;
btnSend.disabled = true;
}
} function dataChannelError(error) {
console.log("Data Channel Error:", error);
} function conn() {
//1 触发socke连接
socket = io.connect(); //2 加入房间后的回调
socket.on('joined', (roomid, id) => { state = 'joined'; createPeerConnection(); btnConn.disabled = true;
btnLeave.disabled = false; console.log("reveive joined message:state=", state);
});
socket.on('otherjoin', (roomid, id) => { if (state === 'joined_unbind') {
createPeerConnection();
} var dataChannelOptions = {
ordered: true, //保证到达顺序
};
//文本聊天
dc = pc.createDataChannel('dataChannel', dataChannelOptions);
dc.onmessage = reveivemsg;
dc.onopen = dataChannelStateChange;
dc.onclose = dataChannelStateChange;
dc.onerror = dataChannelError; state = 'joined_conn'; //媒体协商
call();
console.log("reveive otherjoin message:state=", state);
});
socket.on('full', (roomid, id) => {
console.log('receive full message ', roomid, id); closePeerConnection();
closeLocalMedia(); state = 'leaved'; btnConn.disabled = false;
btnLeave.disabled = true;
console.log("reveive full message:state=", state);
alert("the room is full!");
}); socket.on('leaved', (roomid, id) => { state = 'leaved';
socket.disconnect();
btnConn.disabled = false;
btnLeave.disabled = true;
console.log("reveive leaved message:state=", state);
}); socket.on('bye', (roomid, id) => { state = 'joined_unbind';
closePeerConnection();
console.log("reveive bye message:state=", state);
});
socket.on('disconnect', (socket) => {
console.log('receive disconnect message!', roomid);
if (!(state === 'leaved')) {
closePeerConnection();
closeLocalMedia();
}
state = 'leaved'; });
socket.on('message', (roomid, id, data) => {
console.log(" message=====>", data);
//媒体协商
if (data) {
if (data.type === 'offer') {
pc.setRemoteDescription(new RTCSessionDescription(data));
pc.createAnswer()
.then(getAnswer)
.catch(handleAnswerError);
} else if (data.type === 'answer') {
console.log("reveive client message=====>", data);
pc.setRemoteDescription(new RTCSessionDescription(data));
} else if (data.type === 'candidate') {
var candidate = new RTCIceCandidate({
sdpMLineIndex: data.label,
candidate: data.candidate
});
pc.addIceCandidate(candidate); } else {
console.error('the message is invalid!', data)
}
} console.log("reveive client message", roomid, id, data);
}); socket.emit('join', roomid);
return;
} function start() {
if (!navigator.mediaDevices ||
!navigator.mediaDevices.getUserMedia) {
console.log("getUserMedia is not supported!")
return;
} navigator.mediaDevices.getUserMedia({
video: true,
audio: false
})
.then(function (stream) {
localStream = stream;
localVideo.srcObject = localStream;
conn();
})
.catch(function (err) {
console.error("getUserMedia error:", err);
})
} function leave() {
if (socket) {
socket.emit('leave', roomid);
} //释放资源
closePeerConnection();
closeLocalMedia(); btnConn.disabled = false;
btnLeave.disabled = true;
} //关闭流通道
function closeLocalMedia() {
if (localStream && localStream.getTracks()) {
localStream.getTracks().forEach((track) => {
track.stop();
});
}
localStream = null;
} //关闭本地媒体流链接
function closePeerConnection() {
console.log('close RTCPeerConnection!');
if (pc) {
pc.close();
pc = null;
}
} //创建本地流媒体链接
function createPeerConnection() {
console.log('create RTCPeerConnection!');
if (!pc) {
pc = new RTCPeerConnection({
'iceServers': [{
'urls': 'turn:127.0.0.1:8000',
'credential': '123456',
'username': 'autofelix'
}]
}); pc.onicecandidate = (e) => {
if (e.candidate) {
sendMessage(roomid, {
type: 'candidate',
label: e.candidate.sdpMLineIndex,
id: e.candidate.sdpMid,
candidate: e.candidate.candidate
});
}
} //文本聊天
pc.ondatachannel = e => {
dc = e.channel;
dc.onmessage = reveivemsg;
dc.onopen = dataChannelStateChange;
dc.onclose = dataChannelStateChange;
dc.onerror = dataChannelError;
} pc.ontrack = (e) => {
remoteVideo.srcObject = e.streams[0];
}
} if (pc === null || pc === undefined) {
console.error('pc is null or undefined!');
return;
} if (localStream === null || localStream === undefined) {
console.error('localStream is null or undefined!');
return;
} if (localStream) {
localStream.getTracks().forEach((track) => {
pc.addTrack(track, localStream);
})
}
} //发送文本
function send() {
var data = send_txt.value;
if (data) {
dc.send(data);
}
send_txt.value = "";
chat.value += '<-' + data + '\r\n';
}
</script> </html>

三、文件传输

  • 实时文件的传输与实时文本消息传输的基本原理是一样的,都是使用 RTCDataChannel 对象进行传输

  • 它们的区别一方面是传输数据的类型不一样,另一方面是数据的大小不一样

  • 在传输文件的时候,必须要保证文件传输的有序性和完整性,所以需要设置 ordered 和 maxRetransmits 选项

  • 发送数据如下:

// 创建 RTCDataChannel 对象的选项
var options = {
ordered: true,
maxRetransmits: 30 // 最多尝试重传 30 次
}; // 创建 RTCPeerConnection 对象
var pc = new RTCPeerConnection(); // 方法一:通过通道发送
sendChannel = pc.createDataChannel(name, options);
sendChannel.addEventListener('open', onSendChannelStateChange); //打开之后才可以传输数据
sendChannel.addEventListener('close', onSendChannelStateChange);
sendChannel.send(JSON.stringify({
// 将文件信息以 JSON 格式发磅
type: 'fileinfo',
name: file.name,
size: file.size,
filetype: file.type,
lastmodify: file.lastModified
})); // 方法二:通过arraybuffer发送
var offset = 0; // 偏移量
var chunkSize = 16384; // 每次传输的块大小
var file = fileInput.files[0]; // 要传输的文件,它是通过 HTML 中的 file 获取的 // 创建 fileReader 来读取文件
fileReader = new FileReader(); // 当数据被加载时触发该事件
fileReader.onload = e => {
// 发送数据
dc.send(e.target.result);
offset += e.target.result.byteLength; // 更改已读数据的偏移量 if (offset < file.size) { // 如果文件没有被读完
readSlice(offset); // 读取数据
}
} var readSlice = o => {
const slice = file.slice(offset, o + chunkSize); // 计算数据位置
fileReader.readAsArrayBuffer(slice); // 读取 16K 数据
};
readSlice(0); // 开始读取数据
  • 接收数据如下:

  • 当有数据到达时就会触发该事件就会触发 onmessage 事件

  • 只需要简单地将收到的这块数据 push 到 receiveBuffer 数组中即可

var receiveBuffer = []; // 存放数据的数组
var receiveSize = 0; // 数据大小 onmessage = (event) => {
// 每次事件被触发时,说明有数据来了,将收到的数据放到数组中
receiveBuffer.push(event.data);
// 更新已经收到的数据的长度
receivedSize += event.data.byteLength;
// 如果接收到的字节数与文件大小相同,则创建文件
if (receivedSize === fileSize) { //fileSize 是通过信令传过来的
// 创建文件
var received = new Blob(receiveBuffer, { type: 'application/octet-stream' });
// 将 buffer 和 size 清空,为下一次传文件做准备
receiveBuffer = [];
receiveSize = 0;
// 生成下载地址
downloadAnchor.href = URL.createObjectURL(received);
downloadAnchor.download = fileName;
downloadAnchor.textContent = `Click to download '${fileName}' (${fileSize} bytes)`;
downloadAnchor.style.display = 'block';
}
}

11┃音视频直播系统之 WebRTC 进行文本聊天并实时传输文件的更多相关文章

  1. 5┃音视频直播系统之 WebRTC 中的协议UDP、TCP、RTP、RTCP详解

    一.UDP/TCP 如果让你自己开发一套实时互动直播系统,在选择网络传输协议时,你会选择使用UDP协议还是TCP协议 假如使用 TCP 会怎样呢?在极端网络情况下,TCP 为了传输的可靠性,将会进行反 ...

  2. 12┃音视频直播系统之 WebRTC 实现1对1直播系统实战

    一.搭建 Web 服务器 前面我们已经实现过,但是没有详细说HTTPS服务 首先需要引入了 express 库,它的功能非常强大,用它来实现 Web 服务器非常方便 同时还需要引入 HTTPS 服务, ...

  3. 8┃音视频直播系统之 WebRTC 信令系统实现以及通讯核心并实现视频通话

    一.信令系统 信令系统主要用来进行信令的交换 在通信双方彼此连接.传输媒体数据之前,它们要通过信令服务器交换一些信息,如规范协商 若 A 与 B 要进行音视频通信,那么 A 要知道 B 已经上线了,同 ...

  4. 10┃音视频直播系统之 WebRTC 中的数据统计和绘制统计图形

    一.数据统计 在视频直播中,还有一项比较重要,那就是数据监控 比如开发人员需要知道收了多少包.发了多少包.丢了多少包,以及每路流的流量是多少,才能评估出目前用户使用的音视频产品的服务质量是好还是坏 如 ...

  5. 6┃音视频直播系统之 WebRTC 核心驱动SDP规范协商

    一.什么是SDP SDP(Session Description Protocal)其实就是当数据过来时候,告诉数据自己这里支持的解码方式.传输协议等等,这样数据才能根据正确的方式进行解码使用 SDP ...

  6. 3┃音视频直播系统之浏览器中通过 WebRTC 直播视频实时录制回放下载

    一.录制分类 在音视频会议.在线教育等系统中,录制是一个特别重要的功能 录制一般分为服务端录制和客户端录制 服务端录制:优点是不用担心客户因自身电脑问题造成录制失败(如磁盘空间不足),也不会因录制时抢 ...

  7. 4┃音视频直播系统之浏览器中通过 WebRTC 进行桌面共享

    一.共享桌面原理 共享桌面在直播系统中是一个必备功能 共享者:每秒钟抓取多次屏幕,每次抓取的屏幕都与上一次抓取的屏幕做比较,取它们的差值,然后对差值进行压缩:如果是第一次抓屏或切幕的情况,即本次抓取的 ...

  8. 1┃音视频直播系统之浏览器中通过WebRTC访问摄像头

    一.WebRTC的由来 对于前端开发小伙伴而言,如果用 JavaScript 做音视频处理 在以前是不可想象的,因为首先就要考虑浏览器的性能是否跟得上音视频的采集 但是 Google 作为国际顶尖科技 ...

  9. 2┃音视频直播系统之浏览器中通过 WebRTC 拍照片加滤镜并保存

    一.拍照原理 好多人小时候应该都学过,在几张空白的纸上画同一个物体,并让物体之间稍有一些变化,然后连续快速地翻动这几张纸,它就形成了一个小动画,音视频播放器就是利用这样的原理来播放音视频文件的 播放器 ...

随机推荐

  1. carsim2016事件如何设置

    #carsim2016事件设置# 完成以下功能:车速低于60km/h时,加速,设置节气门开度为0.8,制动主斜体样式缸压力设为0:车速高于120km/h时,制动,设置节气门开度为0,制动主缸压力设置为 ...

  2. IOS中弹出键盘后出现fixed失效现象的解决方案

    概述 这个问题常出现在移动web开发中聊天或者留言页面的绝对定位输入框上,页面超过屏幕大小时候输入框focus状态下(键盘弹出)绝对定位的元素失效,导致页面滚动时候把定位元素一并带走,体验十分不好,在 ...

  3. java堆排序

    直接贴源代码: package com.java.fmd; import java.util.Scanner; public class HeapSort { int[] arr; public st ...

  4. Python窗口学习之搜索框美化

    初学tkinter,感觉这个插件虽然是做界面的,但是没有html,也没有android那么人性化 既没有画圆角长方形的办法也没有添加透明按钮的办法(可能是我没找到) 所以自己用canvas画了两个扇形 ...

  5. 浅谈Web前后端分离的意义

    自然是有很大意义的.下面我可能说的比较多--方便题主能够更全面的了解为什么说是有有意义的.另外,本文是以Java的角度谈前后端分离.放心,大家一定会有种是我了,没错,的感觉. 一.先来明晰下概念 前后 ...

  6. MVVM模式-数据的双向绑定

  7. c语言实现循环单链表

    //初始化 Node*InitList() { Node*head=(Node*)malloc(sizeof(Node)); head->next=NULL; head->data=-1; ...

  8. LC-283

    题目链接:https://leetcode-cn.com/problems/move-zeroes/ 首先想到了快排(简易思想),0为中间点, 把不等于0(注意题目没说不能有负数)的放到中间点的左边, ...

  9. 不仅仅是一把瑞士军刀 —— Apifox的野望和不足

    声明:本文内容不涉及任何 Apifox 的功能介绍,一来网上这方面的文章已经汗牛充栋,二来 Apifox 本身的用户体验做的非常好,对于开发者而言学习成本基本为零. 阮一峰:不管你是前端开发还是后端开 ...

  10. Hyperledger Fabric无系统通道启动及通道的创建和删除

    前言 在Hyperledger Fabric组织的动态添加和删除中,我们已经完成了在运行着的网络中动态添加和删除组织,但目前为止,我们启动 orderer 节点的方式都是通过系统通道的方式,这样自带系 ...