11┃音视频直播系统之 WebRTC 进行文本聊天并实时传输文件
Posted autofelix
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了11┃音视频直播系统之 WebRTC 进行文本聊天并实时传输文件相关的知识,希望对你有一定的参考价值。
〝 古人学问遗无力,少壮功夫老始成 〞
随着5G技术的发展,音视频直播领域发生了翻天覆地的变化,尤其是 2011 年 Google 推出 WebRTC 技术后,大大降低了音视频技术的门槛,你再也不必自己去实现回音消除算法了,也不用自己去实现各种音视频的编解码器了,更不必去考虑跨平台的问题了。如果这篇文章能给你带来一点帮助,希望给飞兔小哥哥一键三连,表示支持,谢谢各位小伙伴们。
目录
一、RTCDataChannel
-
WebRTC
不但可以让你进行音视频通话,而且还可以用它传输普通的二进制数据,比如说可以利用它实现文本聊天、文件的传输等 -
WebRTC
的数据通道(RTCDataChannel)
是专门用来传输除了音视频数据之外的任何数据,模仿了WebSocket
的实现 -
RTCDataChannel
支持的数据类型也非常多,包括:字符串
、Blob
、ArrayBuffer
以及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 进行文本聊天并实时传输文件的主要内容,如果未能解决你的问题,请参考以下文章
4┃音视频直播系统之浏览器中通过 WebRTC 进行桌面共享
4┃音视频直播系统之浏览器中通过 WebRTC 进行桌面共享