11┃音视频直播系统之 WebRTC 进行文本聊天并实时传输文件


一、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 对象





    
    
    
    Document
    



    

本地:

远端:

聊天:

三、文件传输

  • 实时文件的传输与实时文本消息传输的基本原理是一样的,都是使用 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';
    }
}