WebRTC 系列文章 文件共享
这是WebRTC系列文章的第四篇。这次我们来对之前做的一对一视频通话和有文字聊天功能的项目添加文件共享功能。
如果你对WebSocket、ICE、SDP、这些知识还不是很了解的话,推荐你先看下文章末尾的几篇推荐文章。
在此特别感谢 前端李国庆的帮助
环境准备
桌面游览器 | Chrome 80.0.3987.163(正式版本) (64 位) |
手机游览器 | Chrome 80.0.3987.162 |
桌面游览器 | Microsoft Edge 版本 80.0.361.111 |
JDK | 1.8 以上 |
springboot | 2.1.6 |
Gradle | 4.8 |
一台CentOS7 云服务器 | 要有公网IP |
要充分了解WebRTC传输文件的流程 FileReader Api 和 ArrayBuffer、Blob、File类型 这些都是你需要提前掌握的的。
实现思路
首先我们在之前视频通话项目的基础上来做增量功能,尽量不波及已有的功能,所以我们需要一个新的RTC连接用于文件共享。然后我们就是用同一个WebSocket endpoint同时在一个页面维持了两个PeerToPeer连接了。
所以我们的实例程序是可以同时在一个页面上发起视频通话、文字沟通和发送文件的。
首先我们要改下websocket服务器的代码,还是按照我之前说的只增加不修改。
前端js也是,要注意的是RTCPeerConnection的创建和RTCDataChannel使用跟之前视频通话不一样。
FileReader
FileReader 对象允许Web应用程序异步读取存储在用户计算机上的文件(或原始数据缓冲区)的内容,使用 File 或 Blob
对象指定要读取的文件或数据。其中File对象可以是来自用户在一个元素上选择文件后返回的FileList对象,也可以来自拖放操作生成的
DataTransfer对象,还可以是来自在一个HTMLCanvasElement上执行mozGetAsFile()方法后返回结果。重要提示: FileReader仅用于以安全的方式从用户(远程)系统读取文件内容 它不能用于从文件系统中按路径名简单地读取文件。
要在JavaScript中按路径名读取文件,应使用标准Ajax解决方案进行服务器端文件读取,如果读取跨域,则使用CORS权限。
事件处理
FileReader.onabort
处理abort事件。该事件在读取操作被中断时触发。
FileReader.onerror
处理error事件。该事件在读取操作发生错误时触发。
FileReader.onload
处理load事件。该事件在读取操作完成时触发。
FileReader.onloadstart
处理loadstart事件。该事件在读取操作开始时触发。
FileReader.onloadend
处理loadend事件。该事件在读取操作结束时(要么成功,要么失败)触发。
FileReader.onprogress
处理progress事件。该事件在读取Blob时触发。
方法
FileReader.abort()
中止读取操作。在返回时,readyState属性为DONE。
FileReader.readAsArrayBuffer()
开始读取指定的 Blob中的内容, 一旦完成, result 属性中保存的将是被读取文件的 ArrayBuffer 数据对象.
FileReader.readAsBinaryString()
开始读取指定的Blob中的内容。一旦完成,result属性中将包含所读取文件的原始二进制数据。
FileReader.readAsDataURL()
开始读取指定的Blob中的内容。一旦完成,result属性中将包含一个data: URL格式的Base64字符串以表示所读取文件的内容。
FileReader.readAsText()
开始读取指定的Blob中的内容。一旦完成,result属性中将包含一个字符串以表示所读取的文件内容。
在示例项目中我们只会用到FileReader的两三个方法。
RTCDataChannel
RTCDataChannel接口代表在两者之间建立了一个双向数据通道的连接。
1、 可以用RTCDataChannel.createDataChannel()获取RTCDataChannel(使用send()方法一次之能发送DOMString类型且不超出大概856字节的数据,在每两次发送数据间隔小于350毫秒也会抛异常)
2、在现有的 RTCPeerConnection上用RTCDataChannelEvent类型的 datachannel 事件接收,创建出 RTCDataChannel类型的对象。(RTCDataChannel.send()
将参数中的数据通过channel发送。这个数据可以是DOMString, Blob, ArrayBuffer或者是 ArrayBufferView类型。)
yourConnectionForFileShare.ondatachannel=function(event){
var receiveChannel = event.channel;
receiveChannel.onmessage = function(event){
console.log("Got Data Channel Message:", event.data);
// 事件处理代码
}
具体操作步骤如下
当前此示例项目不支持超过16MB的文件
发送方
接收方
码代码
打开数据通道和接收文件事件处理
openFileDataChannel()这里我们将它放在了setupFileSharePeerConnection()
// 打开文件共享数据通道
function openFileDataChannel() {
let currentFileSizeP = 0;
var dataChannelOptions = {
ordered: true,
reliable: true
// ,
// negotiated: true,
// id: 0
};
fileDataChannel = yourConnectionForFileShare.createDataChannel("myFileChannel", dataChannelOptions);
fileDataChannel.onerror = function (error) {
console.log("Data Channel Error:", error);
};
yourConnectionForFileShare.ondatachannel=function(event){
var receiveChannel = event.channel;
receiveChannel.onmessage = function(event){
console.log("Got Data Channel Message:", event.data);
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":
console.log("file receive at end");
saveFile(currentFileMeta, currentFile);
break;
}
} catch (e) {
// Assume this is file content
console.log("正在接收文件 《《《《");
currentFile.push(event.data);
currentFileSizeP += currentFile[currentFile.length - 1].byteLength;
var percentage = Math.floor((currentFileSizeP / currentFileMeta.size) * 100);
statusText.innerHTML = "Receiving... " + percentage + "%";
}
};
};
fileDataChannel.onopen = function () {
console.log("File DataChannel Open");
// fileDataChannel.send(name + " has connected. for file share");
readyText.style.display = "inline-block";
};
fileDataChannel.onclose = function () {
readyText.style.display = "none";
};
}
// 下载文件
function saveFile(meta, data) {
console.log("saveFile ....");
var blob = new Blob(data);
var link = document.createElement('a');
link.href = window.URL.createObjectURL(blob);
link.download = meta.name;
link.click();
}
发送文件
// 发送文件按钮 单击事件处理函数
sendFileButton.addEventListener("click", function (event) {
var files = document.querySelector('#files').files;
if (files.length > 0) {
// 获取文件元数据
var filematett = {
"name": files[0].name,
"type": files[0].type,
"size": files[0].size,
"lastModified": files[0].lastModified,
"lastModifiedDate": files[0].lastModifiedDate,
"webkitRelativePath": files[0].webkitRelativePath
};
// 通知对方:我要开始发送文件了 并把文件元数据发送
dataChannelSend({
type: "start",
data: filematett
});
sendFile(files[0]);
}
});
// 发送文件
function sendFile(file) {
// 发送文件分块的计数
var intii = 0;
// 每个文件分块的大小
var CHUNK_MAX = 163840;
var reader = new FileReader();
// 处理loadend事件。该事件在读取操作结束时(要么成功,要么失败)触发。
reader.onloadend =async function(evt) {
console.log("FileReader 正在加载文件 。。。");
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 + "%";
console.log("++++++++++++++++++++++++++++ -- " + intii++);
// 发送文件块
fileDataChannel.send(buffer.slice(start, end));
// 通知对方文件发送完毕
if (last === true) {
dataChannelSend({
type: "end"
});
} else {
start = end;
// Throttle the sending to avoid flooding
setTimeout(function () {
sendChunk();
}, 50);
}
}
sendChunk();
}
};
// 开始读取指定的 Blob中的内容, 一旦完成, result 属性中保存的将是被读取文件的 ArrayBuffer 数据对象.
reader.readAsArrayBuffer(file);
}
项目仓库
HTML页面为 index2.html
Javascript文件为 part3.js
https://gitee.com/whatitis/WebSocketDemo
推荐阅读