WebRTC音视频开发读书笔记(六)
数据通道不仅可以发送文本消息, 还可以发送图片、二进制文件,将其类型binaryType属性设置成arraybuffer类型即可.
九\、文件传输
1、文件传输流程
(1)使用表单file打开本地文件
(2)使用FileReader读取文件的二进制数据
(3)创建对等连接,本地连接及远端连接
(4)创建发送数据通道
(5)创建接收数据通道
(6)将读取的二进制数据 切割成一个个切片,然后使用数据通道的send方法发送数据 。
(7)使用接收数据通道二进制数据 将其放入数据缓存
(8)根据数据缓存生成Blob 文件
(10)生成文件连接并提供下载。
读取流程中文件二进制数据使用FileReader对象,还需要对数据进行切片处理,在接收方法中使用缓存将接收的数据缓存起来,最后生成blob下载文件。处理流程如下所示:
2、FileReader
FileReader对象允许Web应用程序异步读取存储在用户计算机上的文件内容。常用事件如下所示:
使用File或Blob对象指定要读取的文件或数据,当监听到load事件后,即可通过事件的结果属性拿到数据,然后使用发送通道的send方法将数据发送出去。代码如下:
sendChannel.send(e.target.result)
读取二进制数据可以使用FileReader的readAsArrayBuffer方法,结果为ArrayBuffer对象。还有其它处理方法,如下所示:
读取数据 时需要将数据分成一段一段的,然后发送,接收时再按顺序读取到数据缓存中,数据流的走向如图所示:
上图中数据块叫做Chunk,DataChannel每次发送的数据即为Chunk, 使用对象的slice方法按顺序切割 出数据块。如下面代码所示:
let slice=file.slice(offset,offset+chunksize)
假设chunksize的大小为16348,整个文件的数据块如下所示:
3、发送文件示例
本示例为文件具休步骤如下:
(1)首先在界面上添加input,类型为file,用于选择并读取文件,progress两个,分别用于发送及接收文件进度展示。
(2)创建本地和远端连接,发送和接收数据通道,发送和接收通道的数据类型设置为arraybuffer。
(3)建立本地和远端连接
(4)实例化FileReader对象,并添加如下事件:
error: 读取文件错误
abort: 读取文件取消
load: 文件加载完成
load事件表示数据已经准备好,可以进行切割发送,具体处理逻辑如下所示:
//监听load事件
fileReader.addEventListener('load',(e)=>{...//发送文件数据sendChannel.send(e.target.result);//偏移量offer+=e.target.result.byteLength;...//判断偏移量是否小于文件大小if(offer<file.size){//继续读取readSlice(offser)}
});//读取切片大小
let readSlice=(o)=>{...//开始切片let slice=file.slice(offset ,o+chunksize);//读取二进制数据 fileReader.readAsArrayBuffer(slice);}
(5)数据接收处理,将收到第一个数据放入receiveBuffer缓存,处理逻辑如下所示:
//接收消息处理
onReceiveMessageCallback=(event)=>{
//将接收的数据添加到接收缓存里receiveBuffer.push(event.data);
//设置当前接收文件的大小receivedSize+=event.data.byteLength;...const file=fileInput.files[0];
//判断当前接收的文件大小是否等于文件的大小
if(receivedSize===file.size){//根据缓存生成Blob文件const received=new Blob(receiveBuffer)//将缓存数据设置为空receiveBuffer=[];...//创建下载文件对象及连接...}
}
完整代码如下:
import React from "react";
import { Button } from "antd";//本地连接对象
let localConnection;
//远端连接对象
let remoteConnection;
//发送通道
let sendChannel;
//接收通道
let receiveChannel;
//文件读取
let fileReader;
//接收数据缓存
let receiveBuffer = [];
//接收到的数据大小
let receivedSize = 0;
//文件选择
let fileInput;
//发送进度条
let sendProgress;
//接收进度条
let receiveProgress;/*** 数据通道发送文件示例*/
class DataChannelFile extends React.Component {componentDidMount() {sendProgress =document.getElementById('sendProgress') receiveProgress =document.getElementById('receiveProgress')fileInput = document.getElementById('fileInput');//监听change事件,判断文件是否选择fileInput.addEventListener('change', async () => {const file = fileInput.files[0];if (!file) {console.log('没有选择文件');} else {console.log('选择的文件是:' + file.name);}});}//建立对等连接并发送文件startSendFile = async () => {//创建RTCPeerConnection对象localConnection = new RTCPeerConnection();console.log('创建本地PeerConnection成功:localConnection');//监听返回的Candidate信息localConnection.addEventListener('icecandidate', this.onLocalIceCandidate);//实例化发送通道sendChannel = localConnection.createDataChannel('webrtc-datachannel');//数据类型为二进制sendChannel.binaryType = 'arraybuffer';//onopen事件监听sendChannel.addEventListener('open', this.onSendChannelStateChange);//onclose事件监听sendChannel.addEventListener('close', this.onSendChannelStateChange);//创建RTCPeerConnection对象remoteConnection = new RTCPeerConnection();console.log('创建本地PeerConnection成功:remoteConnection');//监听返回的Candidate信息remoteConnection.addEventListener('icecandidate', this.onRemoteIceCandidate);//远端连接数据到达事件监听remoteConnection.addEventListener('datachannel', this.receiveChannelCallback);//监听ICE状态变化localConnection.addEventListener('iceconnectionstatechange', this.onLocalIceStateChange);//监听ICE状态变化remoteConnection.addEventListener('iceconnectionstatechange', this.onRemoteIceStateChange);try {console.log('localConnection创建提议Offer开始');//创建提议Offerconst offer = await localConnection.createOffer();//创建Offer成功await this.onCreateOfferSuccess(offer);} catch (e) {//创建Offer失败this.onCreateSessionDescriptionError(e);}}//创建会话描述错误onCreateSessionDescriptionError = (error) => {console.log(`创建会话描述SD错误: ${error.toString()}`);}//创建提议Offer成功onCreateOfferSuccess = async (desc) => {//localConnection创建Offer返回的SDP信息console.log(`localConnection创建Offer返回的SDP信息\n${desc.sdp}`);console.log('设置localConnection的本地描述start');try {//设置localConnection的本地描述await localConnection.setLocalDescription(desc);this.onSetLocalSuccess(localConnection);} catch (e) {this.onSetSessionDescriptionError();}console.log('remoteConnection开始设置远端描述');try {//设置remoteConnection的远端描述await remoteConnection.setRemoteDescription(desc);this.onSetRemoteSuccess(remoteConnection);} catch (e) {//创建会话描述错误this.onSetSessionDescriptionError();}console.log('remoteConnection开始创建应答Answer');try {//创建应答Answerconst answer = await remoteConnection.createAnswer();//创建应答成功await this.onCreateAnswerSuccess(answer);} catch (e) {//创建会话描述错误this.onCreateSessionDescriptionError(e);}}//设置本地描述完成onSetLocalSuccess = (pc) => {console.log(`${this.getName(pc)}设置本地描述完成:setLocalDescription`);}//设置远端描述完成onSetRemoteSuccess = (pc) => {console.log(`${this.getName(pc)}设置远端描述完成:setRemoteDescription`);}//设置描述SD错误onSetSessionDescriptionError = (error) => {console.log(`设置描述SD错误: ${error.toString()}`);}getName = (pc) => {return (pc === localConnection) ? 'localConnection' : 'remoteConnection';}//创建应答成功onCreateAnswerSuccess = async (desc) => {//输出SDP信息console.log(`remoteConnection的应答Answer数据:\n${desc.sdp}`);console.log('remoteConnection设置本地描述开始:setLocalDescription');try {//设置remoteConnection的本地描述信息await remoteConnection.setLocalDescription(desc);this.onSetLocalSuccess(remoteConnection);} catch (e) {this.onSetSessionDescriptionError(e);}console.log('localConnection设置远端描述开始:setRemoteDescription');try {//设置localConnection的远端描述,即remoteConnection的应答信息await localConnection.setRemoteDescription(desc);this.onSetRemoteSuccess(localConnection);} catch (e) {this.onSetSessionDescriptionError(e);}}//Candidate事件回调方法onLocalIceCandidate = async (event) => {try {if (event.candidate) {//将会localConnection的Candidate添加至remoteConnection里await remoteConnection.addIceCandidate(event.candidate);this.onAddIceCandidateSuccess(remoteConnection);}} catch (e) {this.onAddIceCandidateError(remoteConnection, e);}console.log(`IceCandidate数据:\n${event.candidate ? event.candidate.candidate : '(null)'}`);}//Candidate事件回调方法onRemoteIceCandidate = async (event) => {try {if (event.candidate) {//将会remoteConnection的Candidate添加至localConnection里await localConnection.addIceCandidate(event.candidate);this.onAddIceCandidateSuccess(localConnection);}} catch (e) {this.onAddIceCandidateError(localConnection, e);}console.log(`IceCandidate数据:\n${event.candidate ? event.candidate.candidate : '(null)'}`);}//添加Candidate成功onAddIceCandidateSuccess = (pc) => {console.log(`${this.getName(pc)}添加IceCandidate成功`);}//添加Candidate失败onAddIceCandidateError = (pc, error) => {console.log(`${this.getName(pc)}添加IceCandidate失败: ${error.toString()}`);}//监听ICE状态变化事件回调方法onLocalIceStateChange = (event) => {console.log(`localConnection连接的ICE状态: ${localConnection.iceConnectionState}`);console.log('ICE状态改变事件: ', event);}//监听ICE状态变化事件回调方法onRemoteIceStateChange = (event) => {console.log(`remoteConnection连接的ICE状态: ${remoteConnection.iceConnectionState}`);console.log('ICE状态改变事件: ', event);}//关闭数据通道closeChannel = () => {console.log('关闭数据通道');sendChannel.close();if (receiveChannel) {receiveChannel.close();}//关闭localConnectionlocalConnection.close();//关闭remoteConnectionremoteConnection.close();//localConnection置为空localConnection = null;//remoteConnection置为空remoteConnection = null;}//发送数据sendData = () => {let file = fileInput.files[0];console.log(`文件是: ${[file.name, file.size, file.type].join(' ')}`);//设置发送进度条的最大值sendProgress.max = file.size;//设置接收进度条的最大值receiveProgress.max = file.size;//文件切片大小,即每次读取的文件大小let chunkSize = 16384;//实例化文件读取对象fileReader = new FileReader();//偏移量可用于表示进度let offset = 0;//监听error事件fileReader.addEventListener('error', (error) => {console.error('读取文件出错:', error)});//监听abort事件fileReader.addEventListener('abort', (event) => {console.log('读取文件取消:', event)});//监听load事件fileReader.addEventListener('load', (e) => {console.log('文件加载完成 ', e);//使用发送通道开始发送文件数据sendChannel.send(e.target.result);//使用文件二进制数据长度作为偏移量offset += e.target.result.byteLength;//使用偏移量作为发送进度sendProgress.value = offset;console.log('当前文件发送进度为:', offset);//判断偏移量是否小于文件大小if (offset < file.size) {//继续读取readSlice(offset);}});//读取切片大小let readSlice = (o) => {console.log('readSlice ', o);//将文件的某一段切割下来,从offset到offset + chunkSize位置切下let slice = file.slice(offset, o + chunkSize);//读取切片的二进制数据fileReader.readAsArrayBuffer(slice);};//首次读取0到chunkSize大小的切片数据readSlice(0);}//接收通道数据到达回调方法receiveChannelCallback = (event) => {//实例化接收通道receiveChannel = event.channel;//数据类型为二进制receiveChannel.binaryType = 'arraybuffer';//接收消息事件监听receiveChannel.onmessage = this.onReceiveMessageCallback;//onopen事件监听receiveChannel.onopen = this.onReceiveChannelStateChange;//onclose事件监听receiveChannel.onclose = this.onReceiveChannelStateChange;receivedSize = 0;}//接收消息处理onReceiveMessageCallback = (event) => {console.log(`接收的数据 ${event.data.byteLength}`);//将接收到的数据添加到接收缓存里receiveBuffer.push(event.data);//设置当前接收文件的大小receivedSize += event.data.byteLength;//使用接收文件的大小表示当前接收进度receiveProgress.value = receivedSize;const file = fileInput.files[0];//判断当前接收的文件大小是否等于文件的大小if (receivedSize === file.size) {//根据缓存数据生成Blob文件const received = new Blob(receiveBuffer);//将缓存数据置为空receiveBuffer = [];//获取下载连接对象let download = document.getElementById('download');//创建下载文件对象及链接download.href = URL.createObjectURL(received);download.download = file.name;download.textContent = `点击下载'${file.name}'(${file.size} bytes)`;download.style.display = 'block';}}//发送通道状态变化onSendChannelStateChange = () => {const readyState = sendChannel.readyState;console.log('发送通道状态: ' + readyState);if (readyState === 'open') {this.sendData();}}//接收通道状态变化onReceiveChannelStateChange = () => {const readyState = receiveChannel.readyState;console.log('接收通道状态:' + readyState);}//取消发送文件cancleSendFile = () => {if (fileReader && fileReader.readyState === 1) {console.log('取消读取文件');fileReader.abort();}}render() {return (<div className="container"><div><form id="fileInfo"><input type="file" id="fileInput" name="files" /></form><div><h2>发送</h2><progress id="sendProgress" max="0" value="0" style={{width:'500px'}}></progress></div><div><h2>接收</h2><progress id="receiveProgress" max="0" value="0" style={{width:'500px'}}></progress></div></div><a id="download"></a><div><Button onClick={this.startSendFile} style={{ marginRight: "10px" }}>发送</Button><Button onClick={this.cancleSendFile} style={{ marginRight: "10px" }}>取消</Button><Button onClick={this.closeChannel} style={{ marginRight: "10px" }}>关闭</Button></div></div>);}
}
//导出组件
export default DataChannelFile;
相关文章:

WebRTC音视频开发读书笔记(六)
数据通道不仅可以发送文本消息, 还可以发送图片、二进制文件,将其类型binaryType属性设置成arraybuffer类型即可. 九\、文件传输 1、文件传输流程 (1)使用表单file打开本地文件 (2)使用FileReader读取文件的二进制数据 &#…...

高级列表组件ReList
高级列表组件ReList 组件实现基于 Vue3 Element Plus Typescript,同时引用 vueUse lodash-es tailwindCss (不影响功能,可忽略) 主要基于JSX风格实现高度动态的列表渲染组件,可以通过信息配置Metas配置控制信息项展示,同时支持…...

Vxe UI vue vxe-table 实现表格数据分组功能,根据字段数据分组
Vxe UI vue vxe-table 实现表格数据分组功能,根据字段数据分组 实现数据分组功能 基于树结构功能就可以直接实现数据分组功能,代码如下: <template><div><vxe-button status"primary" click"listToGroup()&…...

oracle创建账户
1、查看表空间 SELECT tablespace_name FROM user_tablespaces;2、创建用户 CREATE USER FLINKCDC2 IDENTIFIED BY "123456";也可以使用指定表空间的方式 CREATE USER FLINKCDC2 IDENTIFIED BY "123456" DEFAULT TABLESPACE LOGMINER_TBS QUOTA UNLIMIT…...

2024新型数字政府综合解决方案(五)
新型数字政府综合解决方案通过集成人工智能、大数据、区块链和云计算技术,打造了一个智能化、透明化和高效的政务服务平台,旨在提升政府服务的响应速度、处理效率和数据安全性。该方案实现了跨部门的数据共享与实时更新,通过智能化的流程自动…...

datawind可视化查询-其他函数
飞书文档学习链接:https://www.volcengine.com/docs/4726/47275 1. 用户名函数 用户名函数并非 ClickHouse 官方函数,而是与项目用户信息相结合,用于返回当前使用用户的指定信息的函数。 USERNAME()可返回当前用户的用户名,如下所示。该函数也可与其他函数组合使用 2. J…...

数据库MySQL之事务、索引
目录 1.概述 2.事务 3.索引 3.1索引结构 3.2操作语法 1.概述 场景:假如我们需要解散教学部,那么该部门下的所有员工都需要删除。如果教学部成功删除了,但员工出于某些原因(比如SQL语句写错了等)并没有删除,此时就会出现数据…...

AI学习记录 - transformers的decoder和encoder中的自注意力矩阵和掩码矩阵的数据处理
掩码掩码,指的是掩盖住后面的词汇的词向量对我当前词汇造成影响。把PAD字符设置成负无穷大,概念上不叫掩码,只是计算方式和掩码一样。 怎么生成掩码,在非掩码注意力矩阵中,把PAD词向量每个维度设置成负无穷大…...
【Solidity】代币
ERC20 ERC-20 全称 “Ethereum Request for Comment 20”,是一种标准接口,用于实现代币合约。ERC20 标准定义了一组函数和事件,使得代币可以在不同的应用和平台之间互操作。 ERC20 标准接口定义了一组必须实现的函数和事件: in…...
5 - Linux YUM仓库及NFS共享服务
目录 一、YUM概述 1.YUM简介 2.软件仓库的提供方式 3.RPM软件包的来源 4. yum 命令的运用 二、搭建ftp YUM仓库 三、NFS共享服务 1.NFS简述 2.模拟NFS 一、YUM概述 1.YUM简介 YUM(Yellow dog Updater Modified)是一个专门为了解决包的依赖关系…...
上传文件,文件类型限制语法,各种媒体视频文件的Content-Type
各种媒体视频文件的Content-Type “application/x-apple-diskimage”: “DMG”, “application/epubzip”: “EPUB”, “application/java-archive”: “JAR”, “video/x-matroska”: “MKV”, “text/html”: “HTML|HTM”, “text/css”: “CSS”, “text/javascript…...

类和对象(下)(2)
类和对象(下)(2) static成员 • ⽤static修饰的成员变量,称之为静态成员变量,静态成员变量⼀定要在类外进⾏初始化。 • 静态成员变量为当前类的所有对象所共享,不属于某个具体的对象,不存在对象中&#…...

软件测试 - 自动化测试(概念)(Java)(自动化测试分类、web自动化测试、驱动、selenium自动化测试工具的安装)
一、自动化的概念 ⾃动洒⽔机,主要通上⽔就可以⾃动化洒⽔并且可以⾃动的旋转。 ⾃动洗⼿液,免去了⼿动挤压可以⾃动感应出洗⼿液 超市⾃动闸⻔,不需要⼿动的开⻔关⻔ ⽣活中的⾃动化案例有效的减少了⼈⼒的消耗,同时也提⾼了⽣…...
wpf datagrid 实现双向绑定
前台 <DataGridAutoGenerateColumns"False"Background"White"CanUserAddRows"True"Grid.Row"1"RowEditEnding"DataGrid_OnRowEditEnding"RowHeight"60"SelectionUnit"CellOrRowHeader"x:Name"…...

使用循环在el-select下拉框中循环出-3至50
问: 使用循环在el-select下拉框中循环出-3至50 回答: <el-form-itemprop"adPosition"label"广告位置":rules"{required: true, message: 广告位置不能为空, trigger: change}" ><el-select v-model"addDataForm.adPosition"…...

全球海事航行通告解析辅助决策系统
“全球海事航行通告解析辅助决策系统”是一个针对海事行业设计的智能系统,旨在帮助海上导航和航运操作人员解析和应对全球发布的海事航行通告。 要做这样的系统我们必须要了解海事签派员的日常工作。 海事签派员,也称为船舶操作员或船运调度员࿰…...

Spring 解决bean的循环依赖
Spring循环依赖-博客园 1. 什么是循环依赖 2. 循环依赖能引发什么问题 循环依赖可能引发以下问题: 初始化顺序不确定:循环依赖导致无法确定哪个对象应该先被创建和初始化,从而造成初始化顺序的混乱。这可能导致错误的结果或意外的行为。死…...

鸿蒙内核源码分析(ELF格式篇) | 应用程序入口并不是main
阅读之前的说明 先说明,本篇很长,也很枯燥,若不是绝对的技术偏执狂是看不下去的.将通过一段简单代码去跟踪编译成ELF格式后的内容.看看ELF究竟长了怎样的一副花花肠子,用readelf命令去窥视ELF的全貌,最后用objdump命令…...
seq2seq编码器encoder和解码器decoder详解
编码器 在序列到序列模型中,编码器将输入序列(如一个句子)转换为一个隐藏状态序列,供解码器生成输出。编码层通常由嵌入层和RNN(如GRU/LSTM)等组成 Token:是模型处理文本时的基本单元,可以是词,子词,字符…...

前端使用 Konva 实现可视化设计器(21)- 绘制图形(椭圆)
本章开始补充一些基础的图形绘制,比如绘制:直线、曲线、圆/椭形、矩形。这一章主要分享一下本示例是如何开始绘制一个图形的,并以绘制圆/椭形为实现目标。 请大家动动小手,给我一个免费的 Star 吧~ 大家如果发现了 Bug,…...

使用docker在3台服务器上搭建基于redis 6.x的一主两从三台均是哨兵模式
一、环境及版本说明 如果服务器已经安装了docker,则忽略此步骤,如果没有安装,则可以按照一下方式安装: 1. 在线安装(有互联网环境): 请看我这篇文章 传送阵>> 点我查看 2. 离线安装(内网环境):请看我这篇文章 传送阵>> 点我查看 说明:假设每台服务器已…...

python/java环境配置
环境变量放一起 python: 1.首先下载Python Python下载地址:Download Python | Python.org downloads ---windows -- 64 2.安装Python 下面两个,然后自定义,全选 可以把前4个选上 3.环境配置 1)搜高级系统设置 2…...

PPT|230页| 制造集团企业供应链端到端的数字化解决方案:从需求到结算的全链路业务闭环构建
制造业采购供应链管理是企业运营的核心环节,供应链协同管理在供应链上下游企业之间建立紧密的合作关系,通过信息共享、资源整合、业务协同等方式,实现供应链的全面管理和优化,提高供应链的效率和透明度,降低供应链的成…...

剑指offer20_链表中环的入口节点
链表中环的入口节点 给定一个链表,若其中包含环,则输出环的入口节点。 若其中不包含环,则输出null。 数据范围 节点 val 值取值范围 [ 1 , 1000 ] [1,1000] [1,1000]。 节点 val 值各不相同。 链表长度 [ 0 , 500 ] [0,500] [0,500]。 …...
鸿蒙中用HarmonyOS SDK应用服务 HarmonyOS5开发一个医院查看报告小程序
一、开发环境准备 工具安装: 下载安装DevEco Studio 4.0(支持HarmonyOS 5)配置HarmonyOS SDK 5.0确保Node.js版本≥14 项目初始化: ohpm init harmony/hospital-report-app 二、核心功能模块实现 1. 报告列表…...
python如何将word的doc另存为docx
将 DOCX 文件另存为 DOCX 格式(Python 实现) 在 Python 中,你可以使用 python-docx 库来操作 Word 文档。不过需要注意的是,.doc 是旧的 Word 格式,而 .docx 是新的基于 XML 的格式。python-docx 只能处理 .docx 格式…...

【Zephyr 系列 10】实战项目:打造一个蓝牙传感器终端 + 网关系统(完整架构与全栈实现)
🧠关键词:Zephyr、BLE、终端、网关、广播、连接、传感器、数据采集、低功耗、系统集成 📌目标读者:希望基于 Zephyr 构建 BLE 系统架构、实现终端与网关协作、具备产品交付能力的开发者 📊篇幅字数:约 5200 字 ✨ 项目总览 在物联网实际项目中,**“终端 + 网关”**是…...

【JavaWeb】Docker项目部署
引言 之前学习了Linux操作系统的常见命令,在Linux上安装软件,以及如何在Linux上部署一个单体项目,大多数同学都会有相同的感受,那就是麻烦。 核心体现在三点: 命令太多了,记不住 软件安装包名字复杂&…...
全面解析各类VPN技术:GRE、IPsec、L2TP、SSL与MPLS VPN对比
目录 引言 VPN技术概述 GRE VPN 3.1 GRE封装结构 3.2 GRE的应用场景 GRE over IPsec 4.1 GRE over IPsec封装结构 4.2 为什么使用GRE over IPsec? IPsec VPN 5.1 IPsec传输模式(Transport Mode) 5.2 IPsec隧道模式(Tunne…...

10-Oracle 23 ai Vector Search 概述和参数
一、Oracle AI Vector Search 概述 企业和个人都在尝试各种AI,使用客户端或是内部自己搭建集成大模型的终端,加速与大型语言模型(LLM)的结合,同时使用检索增强生成(Retrieval Augmented Generation &#…...